diff options
Diffstat (limited to 'spec')
105 files changed, 3840 insertions, 331 deletions
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb index 64b9af7b845..fb76b7fdf38 100644 --- a/spec/controllers/projects/blob_controller_spec.rb +++ b/spec/controllers/projects/blob_controller_spec.rb @@ -1,6 +1,8 @@ require 'rails_helper' describe Projects::BlobController do + include ProjectForksHelper + let(:project) { create(:project, :public, :repository) } describe "GET show" do @@ -226,9 +228,8 @@ describe Projects::BlobController do end context 'when user has forked project' do - let(:forked_project_link) { create(:forked_project_link, forked_from_project: project) } - let!(:forked_project) { forked_project_link.forked_to_project } - let(:guest) { forked_project.owner } + let!(:forked_project) { fork_project(project, guest, namespace: guest.namespace, repository: true) } + let(:guest) { create(:user) } before do sign_in(guest) diff --git a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb index fad2c8f3ab7..7260350d5fb 100644 --- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb +++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Projects::MergeRequests::DiffsController do + include ProjectForksHelper + let(:project) { create(:project, :repository) } let(:user) { project.owner } let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } @@ -37,12 +39,12 @@ describe Projects::MergeRequests::DiffsController do render_views let(:project) { create(:project, :repository) } - let(:fork_project) { create(:forked_project_with_submodules) } - let(:merge_request) { create(:merge_request_with_diffs, source_project: fork_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) } + let(:forked_project) { fork_project_with_submodules(project) } + let(:merge_request) { create(:merge_request_with_diffs, source_project: forked_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) } before do - fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id) - fork_project.save + project.add_developer(user) + merge_request.reload go end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index e46d1995498..707e7c32283 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Projects::MergeRequestsController do + include ProjectForksHelper + let(:project) { create(:project, :repository) } let(:user) { project.owner } let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } @@ -204,14 +206,11 @@ describe Projects::MergeRequestsController do context 'there is no source project' do let(:project) { create(:project, :repository) } - let(:fork_project) { create(:forked_project_with_submodules) } - let(:merge_request) { create(:merge_request, source_project: fork_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) } + let(:forked_project) { fork_project_with_submodules(project) } + let!(:merge_request) { create(:merge_request, source_project: forked_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) } before do - fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id) - fork_project.save - merge_request.reload - fork_project.destroy + forked_project.destroy end it 'closes MR without errors' do @@ -599,21 +598,16 @@ describe Projects::MergeRequestsController do describe 'GET ci_environments_status' do context 'the environment is from a forked project' do - let!(:forked) { create(:project, :repository) } + let!(:forked) { fork_project(project, user, repository: true) } let!(:environment) { create(:environment, project: forked) } let!(:deployment) { create(:deployment, environment: environment, sha: forked.commit.id, ref: 'master') } let(:admin) { create(:admin) } let(:merge_request) do - create(:forked_project_link, forked_to_project: forked, - forked_from_project: project) - create(:merge_request, source_project: forked, target_project: project) end before do - forked.team << [user, :master] - get :ci_environments_status, namespace_id: merge_request.project.namespace.to_param, project_id: merge_request.project, diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index e3114e5c06e..135fd6449ff 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Projects::NotesController do + include ProjectForksHelper + let(:user) { create(:user) } let(:project) { create(:project) } let(:issue) { create(:issue, project: project) } @@ -210,18 +212,16 @@ describe Projects::NotesController do context 'when creating a commit comment from an MR fork' do let(:project) { create(:project, :repository) } - let(:fork_project) do - create(:project, :repository).tap do |fork| - create(:forked_project_link, forked_to_project: fork, forked_from_project: project) - end + let(:forked_project) do + fork_project(project, nil, repository: true) end let(:merge_request) do - create(:merge_request, source_project: fork_project, target_project: project, source_branch: 'feature', target_branch: 'master') + create(:merge_request, source_project: forked_project, target_project: project, source_branch: 'feature', target_branch: 'master') end let(:existing_comment) do - create(:note_on_commit, note: 'a note', project: fork_project, commit_id: merge_request.commit_shas.first) + create(:note_on_commit, note: 'a note', project: forked_project, commit_id: merge_request.commit_shas.first) end def post_create(extra_params = {}) @@ -231,7 +231,7 @@ describe Projects::NotesController do project_id: project, target_type: 'merge_request', target_id: merge_request.id, - note_project_id: fork_project.id, + note_project_id: forked_project.id, in_reply_to_discussion_id: existing_comment.discussion_id }.merge(extra_params) end @@ -253,16 +253,16 @@ describe Projects::NotesController do end context 'when the user has access to the fork' do - let(:discussion) { fork_project.notes.find_discussion(existing_comment.discussion_id) } + let(:discussion) { forked_project.notes.find_discussion(existing_comment.discussion_id) } before do - fork_project.add_developer(user) + forked_project.add_developer(user) existing_comment end it 'creates the note' do - expect { post_create }.to change { fork_project.notes.count }.by(1) + expect { post_create }.to change { forked_project.notes.count }.by(1) end end end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 491f35d0fb6..d7148e888ab 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -1,6 +1,8 @@ require('spec_helper') describe ProjectsController do + include ProjectForksHelper + let(:project) { create(:project) } let(:public_project) { create(:project, :public) } let(:user) { create(:user) } @@ -377,10 +379,10 @@ describe ProjectsController do context "when the project is forked" do let(:project) { create(:project, :repository) } - let(:fork_project) { create(:project, :repository, forked_from_project: project) } + let(:forked_project) { fork_project(project, nil, repository: true) } let(:merge_request) do create(:merge_request, - source_project: fork_project, + source_project: forked_project, target_project: project) end @@ -388,7 +390,7 @@ describe ProjectsController do project.merge_requests << merge_request sign_in(admin) - delete :destroy, namespace_id: fork_project.namespace, id: fork_project + delete :destroy, namespace_id: forked_project.namespace, id: forked_project expect(merge_request.reload.state).to eq('closed') end @@ -455,18 +457,14 @@ describe ProjectsController do end context 'with forked project' do - let(:project_fork) { create(:project, :repository, namespace: user.namespace) } - - before do - create(:forked_project_link, forked_to_project: project_fork) - end + let(:forked_project) { fork_project(create(:project, :public), user) } it 'removes fork from project' do delete(:remove_fork, - namespace_id: project_fork.namespace.to_param, - id: project_fork.to_param, format: :js) + namespace_id: forked_project.namespace.to_param, + id: forked_project.to_param, format: :js) - expect(project_fork.forked?).to be_falsey + expect(forked_project.reload.forked?).to be_falsey expect(flash[:notice]).to eq('The fork relationship has been removed.') expect(response).to render_template(:remove_fork) end diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb index 5a4ab39ab86..1d3ddfbd220 100644 --- a/spec/controllers/registrations_controller_spec.rb +++ b/spec/controllers/registrations_controller_spec.rb @@ -76,12 +76,68 @@ describe RegistrationsController do sign_in(user) end - it 'schedules the user for destruction' do - expect(DeleteUserWorker).to receive(:perform_async).with(user.id, user.id, {}) + def expect_failure(message) + expect(flash[:alert]).to eq(message) + expect(response.status).to eq(303) + expect(response).to redirect_to profile_account_path + end + + def expect_password_failure + expect_failure('Invalid password') + end + + def expect_username_failure + expect_failure('Invalid username') + end + + def expect_success + expect(flash[:notice]).to eq 'Account scheduled for removal.' + expect(response.status).to eq(303) + expect(response).to redirect_to new_user_session_path + end - post(:destroy) + context 'user requires password confirmation' do + it 'fails if password confirmation is not provided' do + post :destroy - expect(response.status).to eq(302) + expect_password_failure + end + + it 'fails if password confirmation is wrong' do + post :destroy, password: 'wrong password' + + expect_password_failure + end + + it 'succeeds if password is confirmed' do + post :destroy, password: '12345678' + + expect_success + end + end + + context 'user does not require password confirmation' do + before do + stub_application_setting(password_authentication_enabled: false) + end + + it 'fails if username confirmation is not provided' do + post :destroy + + expect_username_failure + end + + it 'fails if username confirmation is wrong' do + post :destroy, username: 'wrong username' + + expect_username_failure + end + + it 'succeeds if username is confirmed' do + post :destroy, username: user.username + + expect_success + end end end end diff --git a/spec/factories/ci/build_trace_section_names.rb b/spec/factories/ci/build_trace_section_names.rb new file mode 100644 index 00000000000..1c16225f0e5 --- /dev/null +++ b/spec/factories/ci/build_trace_section_names.rb @@ -0,0 +1,6 @@ +FactoryGirl.define do + factory :ci_build_trace_section_name, class: Ci::BuildTraceSectionName do + sequence(:name) { |n| "section_#{n}" } + project factory: :project + end +end diff --git a/spec/factories/fork_networks.rb b/spec/factories/fork_networks.rb new file mode 100644 index 00000000000..f42d36f3d19 --- /dev/null +++ b/spec/factories/fork_networks.rb @@ -0,0 +1,5 @@ +FactoryGirl.define do + factory :fork_network do + association :root_project, factory: :project + end +end diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index cbec716d6ea..2c732aaf4ed 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -22,6 +22,11 @@ FactoryGirl.define do trait :with_diffs do end + trait :with_image_diffs do + source_branch "add_images_and_changes" + target_branch "master" + end + trait :without_diffs do source_branch "improve/awesome" target_branch "master" diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 958d62181a2..4034e7905ad 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -149,7 +149,7 @@ FactoryGirl.define do end end - trait :readonly do + trait :read_only do repository_read_only true end diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb index 8204828b5b9..d26428ec286 100644 --- a/spec/features/dashboard/merge_requests_spec.rb +++ b/spec/features/dashboard/merge_requests_spec.rb @@ -3,12 +3,13 @@ require 'spec_helper' feature 'Dashboard Merge Requests' do include FilterItemSelectHelper include SortingHelper + include ProjectForksHelper let(:current_user) { create :user } let(:project) { create(:project) } let(:public_project) { create(:project, :public, :repository) } - let(:forked_project) { Projects::ForkService.new(public_project, current_user).execute } + let(:forked_project) { fork_project(public_project, current_user, repository: true) } before do project.add_master(current_user) diff --git a/spec/features/issues/issue_detail_spec.rb b/spec/features/issues/issue_detail_spec.rb index 28b636f9359..c0c396af93f 100644 --- a/spec/features/issues/issue_detail_spec.rb +++ b/spec/features/issues/issue_detail_spec.rb @@ -28,8 +28,7 @@ feature 'Issue Detail', :js do fill_in 'issue-title', with: 'issue title' click_button 'Save' - visit profile_account_path - click_link 'Delete account' + Users::DestroyService.new(user).execute(user) visit project_issue_path(project, issue) end diff --git a/spec/features/merge_requests/create_new_mr_from_fork_spec.rb b/spec/features/merge_requests/create_new_mr_from_fork_spec.rb new file mode 100644 index 00000000000..93c40ff6443 --- /dev/null +++ b/spec/features/merge_requests/create_new_mr_from_fork_spec.rb @@ -0,0 +1,89 @@ +require 'spec_helper' + +feature 'Creating a merge request from a fork', :js do + include ProjectForksHelper + + let(:user) { create(:user) } + let(:project) { create(:project, :public, :repository) } + let!(:source_project) do + fork_project(project, user, + repository: true, + namespace: user.namespace) + end + + before do + source_project.add_master(user) + + sign_in(user) + end + + shared_examples 'create merge request to other project' do + it 'has all possible target projects' do + visit project_new_merge_request_path(source_project) + + first('.js-target-project').click + + within('.dropdown-target-project .dropdown-content') do + expect(page).to have_content(project.full_path) + expect(page).to have_content(target_project.full_path) + expect(page).to have_content(source_project.full_path) + end + end + + it 'allows creating the merge request to another target project' do + visit project_merge_requests_path(source_project) + + page.within '.content' do + click_link 'New merge request' + end + + find('.js-source-branch', match: :first).click + find('.dropdown-source-branch .dropdown-content a', match: :first).click + + first('.js-target-project').click + find('.dropdown-target-project .dropdown-content a', text: target_project.full_path).click + + click_button 'Compare branches and continue' + + wait_for_requests + + expect { click_button 'Submit merge request' } + .to change { target_project.merge_requests.reload.size }.by(1) + end + + it 'updates the branches when selecting a new target project' do + target_project_member = target_project.owner + CreateBranchService.new(target_project, target_project_member) + .execute('a-brand-new-branch-to-test', 'master') + visit project_new_merge_request_path(source_project) + + first('.js-target-project').click + find('.dropdown-target-project .dropdown-content a', text: target_project.full_path).click + + wait_for_requests + + first('.js-target-branch').click + + within('.dropdown-target-branch .dropdown-content') do + expect(page).to have_content('a-brand-new-branch-to-test') + end + end + end + + context 'creating to the source of a fork' do + let!(:target_project) { project } + + it_behaves_like('create merge request to other project') + end + + context 'creating to a sibling of a fork' do + let!(:target_project) do + other_user = create(:user) + fork_project(project, other_user, + repository: true, + namespace: other_user.namespace) + end + + it_behaves_like('create merge request to other project') + end +end diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb index 09541873f71..f9bc3ee6c58 100644 --- a/spec/features/merge_requests/created_from_fork_spec.rb +++ b/spec/features/merge_requests/created_from_fork_spec.rb @@ -1,21 +1,20 @@ require 'spec_helper' feature 'Merge request created from fork' do + include ProjectForksHelper + given(:user) { create(:user) } given(:project) { create(:project, :public, :repository) } - given(:fork_project) { create(:project, :public, :repository) } + given(:forked_project) { fork_project(project, user, repository: true) } given!(:merge_request) do - create(:forked_project_link, forked_to_project: fork_project, - forked_from_project: project) - - create(:merge_request_with_diffs, source_project: fork_project, + create(:merge_request_with_diffs, source_project: forked_project, target_project: project, description: 'Test merge request') end background do - fork_project.team << [user, :master] + forked_project.team << [user, :master] sign_in user end @@ -31,7 +30,7 @@ feature 'Merge request created from fork' do background do create(:note_on_commit, note: comment, - project: fork_project, + project: forked_project, commit_id: merge_request.commit_shas.first) end @@ -55,7 +54,7 @@ feature 'Merge request created from fork' do context 'source project is deleted' do background do MergeRequests::MergeService.new(project, user).execute(merge_request) - fork_project.destroy! + forked_project.destroy! end scenario 'user can access merge request', js: true do @@ -69,7 +68,7 @@ feature 'Merge request created from fork' do context 'pipeline present in source project' do given(:pipeline) do create(:ci_pipeline, - project: fork_project, + project: forked_project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch) end diff --git a/spec/features/merge_requests/diffs_spec.rb b/spec/features/merge_requests/diffs_spec.rb index e9068f722d5..ee9bb50a881 100644 --- a/spec/features/merge_requests/diffs_spec.rb +++ b/spec/features/merge_requests/diffs_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' feature 'Diffs URL', js: true do + include ProjectForksHelper + let(:project) { create(:project, :public, :repository) } let(:merge_request) { create(:merge_request, source_project: project) } @@ -42,8 +44,12 @@ feature 'Diffs URL', js: true do visit "#{diffs_project_merge_request_path(project, merge_request)}#{fragment}" end - it 'shows expanded note' do - expect(page).to have_selector(fragment, visible: true) + it 'shows collapsed note' do + wait_for_requests + + expect(page).to have_selector('.discussion-notes.collapsed') do |note_container| + expect(note_container).to have_selector(fragment, visible: false) + end end end end @@ -64,7 +70,7 @@ feature 'Diffs URL', js: true do context 'when editing file' do let(:author_user) { create(:user) } let(:user) { create(:user) } - let(:forked_project) { Projects::ForkService.new(project, author_user).execute } + let(:forked_project) { fork_project(project, author_user, repository: true) } let(:merge_request) { create(:merge_request_with_diffs, source_project: forked_project, target_project: project, author: author_user) } let(:changelog_id) { Digest::SHA1.hexdigest("CHANGELOG") } diff --git a/spec/features/merge_requests/form_spec.rb b/spec/features/merge_requests/form_spec.rb index de98b147d04..758fc9b139d 100644 --- a/spec/features/merge_requests/form_spec.rb +++ b/spec/features/merge_requests/form_spec.rb @@ -1,8 +1,10 @@ require 'rails_helper' describe 'New/edit merge request', :js do + include ProjectForksHelper + let!(:project) { create(:project, :public, :repository) } - let(:fork_project) { create(:project, :repository, forked_from_project: project) } + let(:forked_project) { fork_project(project, nil, repository: true) } let!(:user) { create(:user) } let!(:user2) { create(:user) } let!(:milestone) { create(:milestone, project: project) } @@ -170,16 +172,16 @@ describe 'New/edit merge request', :js do context 'forked project' do before do - fork_project.team << [user, :master] + forked_project.team << [user, :master] sign_in(user) end context 'new merge request' do before do visit project_new_merge_request_path( - fork_project, + forked_project, merge_request: { - source_project_id: fork_project.id, + source_project_id: forked_project.id, target_project_id: project.id, source_branch: 'fix', target_branch: 'master' @@ -238,7 +240,7 @@ describe 'New/edit merge request', :js do context 'edit merge request' do before do merge_request = create(:merge_request, - source_project: fork_project, + source_project: forked_project, target_project: project, source_branch: 'fix', target_branch: 'master' diff --git a/spec/features/merge_requests/image_diff_notes.rb b/spec/features/merge_requests/image_diff_notes.rb new file mode 100644 index 00000000000..e8ca8a98d70 --- /dev/null +++ b/spec/features/merge_requests/image_diff_notes.rb @@ -0,0 +1,196 @@ +require 'spec_helper' + +feature 'image diff notes', js: true do + include NoteInteractionHelpers + + let(:user) { create(:user) } + let(:project) { create(:project, :public, :repository) } + + before do + project.team << [user, :master] + sign_in user + + page.driver.set_cookie('sidebar_collapsed', 'true') + + # Stub helper to return any blob file as image from public app folder. + # This is necessary to run this specs since we don't display repo images in capybara. + allow_any_instance_of(DiffHelper).to receive(:diff_file_blob_raw_path).and_return('/apple-touch-icon.png') + end + + context 'create commit diff notes' do + commit_id = '2f63565e7aac07bcdadb654e253078b727143ec4' + + describe 'create a new diff note' do + before do + visit project_commit_path(project, commit_id) + create_image_diff_note + end + + it 'shows indicator badge on image diff' do + indicator = find('.js-image-badge') + + expect(indicator).to have_content('1') + end + + it 'shows the avatar badge on the new note' do + badge = find('.image-diff-avatar-link .badge') + + expect(badge).to have_content('1') + end + + it 'allows collapsing/expanding the discussion notes' do + find('.js-diff-notes-toggle', :first).click + + expect(page).not_to have_content('image diff test comment') + + find('.js-diff-notes-toggle').click + + expect(page).to have_content('image diff test comment') + end + end + + describe 'render commit diff notes' do + let(:path) { "files/images/6049019_460s.jpg" } + let(:commit) { project.commit('2f63565e7aac07bcdadb654e253078b727143ec4') } + + let(:note1_position) do + Gitlab::Diff::Position.new( + old_path: path, + new_path: path, + width: 100, + height: 100, + x: 10, + y: 10, + position_type: "image", + diff_refs: commit.diff_refs + ) + end + + let(:note2_position) do + Gitlab::Diff::Position.new( + old_path: path, + new_path: path, + width: 100, + height: 100, + x: 20, + y: 20, + position_type: "image", + diff_refs: commit.diff_refs + ) + end + + let!(:note1) { create(:diff_note_on_commit, commit_id: commit.id, project: project, position: note1_position, note: 'my note 1') } + let!(:note2) { create(:diff_note_on_commit, commit_id: commit.id, project: project, position: note2_position, note: 'my note 2') } + + before do + visit project_commit_path(project, commit.id) + wait_for_requests + end + + it 'render diff indicators within the image diff frame' do + expect(page).to have_css('.js-image-badge', count: 2) + end + + it 'shows the diff notes' do + expect(page).to have_css('.diff-content .note', count: 2) + end + + it 'shows the diff notes with correct avatar badge numbers' do + expect(page).to have_css('.image-diff-avatar-link', text: 1) + expect(page).to have_css('.image-diff-avatar-link', text: 2) + end + end + end + + %w(inline parallel).each do |view| + context "#{view} view" do + let(:merge_request) { create(:merge_request_with_diffs, :with_image_diffs, source_project: project, author: user) } + let(:path) { "files/images/ee_repo_logo.png" } + + let(:position) do + Gitlab::Diff::Position.new( + old_path: path, + new_path: path, + width: 100, + height: 100, + x: 1, + y: 1, + position_type: "image", + diff_refs: merge_request.diff_refs + ) + end + + let!(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position) } + + describe 'creating a new diff note' do + before do + visit diffs_project_merge_request_path(project, merge_request, view: view) + create_image_diff_note + end + + it 'shows indicator badge on image diff' do + indicator = find('.js-image-badge', match: :first) + + expect(indicator).to have_content('1') + end + + it 'shows the avatar badge on the new note' do + badge = find('.image-diff-avatar-link .badge', match: :first) + + expect(badge).to have_content('1') + end + + it 'allows expanding/collapsing the discussion notes' do + page.all('.js-diff-notes-toggle')[0].trigger('click') + page.all('.js-diff-notes-toggle')[1].trigger('click') + + expect(page).not_to have_content('image diff test comment') + + page.all('.js-diff-notes-toggle')[0].trigger('click') + page.all('.js-diff-notes-toggle')[1].trigger('click') + + expect(page).to have_content('image diff test comment') + end + end + end + end + + describe 'discussion tab polling', :js do + let(:merge_request) { create(:merge_request_with_diffs, :with_image_diffs, source_project: project, author: user) } + let(:path) { "files/images/ee_repo_logo.png" } + + let(:position) do + Gitlab::Diff::Position.new( + old_path: path, + new_path: path, + width: 100, + height: 100, + x: 50, + y: 50, + position_type: "image", + diff_refs: merge_request.diff_refs + ) + end + + before do + visit project_merge_request_path(project, merge_request) + end + + it 'render diff indicators within the image frame' do + diff_note = create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position) + + wait_for_requests + + expect(page).to have_selector('.image-comment-badge') + expect(page).to have_content(diff_note.note) + end + end +end + +def create_image_diff_note + find('.js-add-image-diff-note-button', match: :first).click + page.all('.js-add-image-diff-note-button')[0].trigger('click') + find('.diff-content .note-textarea').native.send_keys('image diff test comment') + click_button 'Comment' + wait_for_requests +end diff --git a/spec/features/merge_requests/user_posts_diff_notes_spec.rb b/spec/features/merge_requests/user_posts_diff_notes_spec.rb index e1ca4fe186c..7a773fb2baa 100644 --- a/spec/features/merge_requests/user_posts_diff_notes_spec.rb +++ b/spec/features/merge_requests/user_posts_diff_notes_spec.rb @@ -227,6 +227,7 @@ feature 'Merge requests > User posts diff notes', :js do write_comment_on_line(line_holder, diff_side) click_button 'Comment' + wait_for_requests assert_comment_persistence(line_holder, asset_form_reset: asset_form_reset) diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb index f183dd8cb75..1cddd35fd8a 100644 --- a/spec/features/profile_spec.rb +++ b/spec/features/profile_spec.rb @@ -12,11 +12,47 @@ describe 'Profile account page' do visit profile_account_path end - it { expect(page).to have_content('Remove account') } + it { expect(page).to have_content('Delete account') } - it 'deletes the account' do - expect { click_link 'Delete account' }.to change { User.where(id: user.id).count }.by(-1) - expect(current_path).to eq(new_user_session_path) + it 'does not immediately delete the account' do + click_button 'Delete account' + + expect(User.exists?(user.id)).to be_truthy + end + + it 'deletes user', :js do + click_button 'Delete account' + + fill_in 'password', with: '12345678' + + page.within '.popup-dialog' do + click_button 'Delete account' + end + + expect(page).to have_content('Account scheduled for removal') + expect(User.exists?(user.id)).to be_falsy + end + + it 'shows invalid password flash message', :js do + click_button 'Delete account' + + fill_in 'password', with: 'testing123' + + page.within '.popup-dialog' do + click_button 'Delete account' + end + + expect(page).to have_content('Invalid password') + end + + it 'does not show delete button when user owns a group' do + group = create(:group) + group.add_owner(user) + + visit profile_account_path + + expect(page).not_to have_button('Delete account') + expect(page).to have_content("Your account is currently an owner in these groups: #{group.name}") end end diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb index 1f9b52dd998..c9b8ef4e37b 100644 --- a/spec/features/projects/issuable_templates_spec.rb +++ b/spec/features/projects/issuable_templates_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' feature 'issuable templates', js: true do + include ProjectForksHelper + let(:user) { create(:user) } let(:project) { create(:project, :public, :repository) } let(:issue_form_location) { '#content-body .issuable-details .detail-page-description' } @@ -116,15 +118,13 @@ feature 'issuable templates', js: true do context 'user creates a merge request from a forked project using templates' do let(:template_content) { 'this is a test "feature-proposal" template' } let(:fork_user) { create(:user) } - let(:fork_project) { create(:project, :public, :repository) } - let(:merge_request) { create(:merge_request, :with_diffs, source_project: fork_project, target_project: project) } + let(:forked_project) { fork_project(project, fork_user) } + let(:merge_request) { create(:merge_request, :with_diffs, source_project: forked_project, target_project: project) } background do sign_out(:user) project.team << [fork_user, :developer] - fork_project.team << [fork_user, :master] - create(:forked_project_link, forked_to_project: fork_project, forked_from_project: project) sign_in(fork_user) diff --git a/spec/features/projects/user_creates_directory_spec.rb b/spec/features/projects/user_creates_directory_spec.rb index 1ba5d83eadf..e8f2e4813c5 100644 --- a/spec/features/projects/user_creates_directory_spec.rb +++ b/spec/features/projects/user_creates_directory_spec.rb @@ -79,7 +79,7 @@ feature 'User creates a directory', js: true do fill_in(:commit_message, with: 'New commit message', visible: true) click_button('Create directory') - fork = user.fork_of(project2) + fork = user.fork_of(project2.reload) expect(current_path).to eq(project_new_merge_request_path(fork)) end diff --git a/spec/features/projects/user_creates_files_spec.rb b/spec/features/projects/user_creates_files_spec.rb index 3d335687510..51d918bc85d 100644 --- a/spec/features/projects/user_creates_files_spec.rb +++ b/spec/features/projects/user_creates_files_spec.rb @@ -142,7 +142,7 @@ describe 'User creates files' do fill_in(:commit_message, with: 'New commit message', visible: true) click_button('Commit changes') - fork = user.fork_of(project2) + fork = user.fork_of(project2.reload) expect(current_path).to eq(project_new_merge_request_path(fork)) expect(page).to have_content('New commit message') diff --git a/spec/features/projects/user_deletes_files_spec.rb b/spec/features/projects/user_deletes_files_spec.rb index 95cd316be0e..7f48a69d9b7 100644 --- a/spec/features/projects/user_deletes_files_spec.rb +++ b/spec/features/projects/user_deletes_files_spec.rb @@ -59,7 +59,7 @@ describe 'User deletes files' do fill_in(:commit_message, with: 'New commit message', visible: true) click_button('Delete file') - fork = user.fork_of(project2) + fork = user.fork_of(project2.reload) expect(current_path).to eq(project_new_merge_request_path(fork)) expect(page).to have_content('New commit message') diff --git a/spec/features/projects/user_edits_files_spec.rb b/spec/features/projects/user_edits_files_spec.rb index 19954313c23..2798041fa0c 100644 --- a/spec/features/projects/user_edits_files_spec.rb +++ b/spec/features/projects/user_edits_files_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe 'User edits files' do + include ProjectForksHelper let(:project) { create(:project, :repository, name: 'Shop') } let(:project2) { create(:project, :repository, name: 'Another Project', path: 'another-project') } let(:project_tree_path_root_ref) { project_tree_path(project, project.repository.root_ref) } @@ -122,7 +123,7 @@ describe 'User edits files' do fill_in(:commit_message, with: 'New commit message', visible: true) click_button('Commit changes') - fork = user.fork_of(project2) + fork = user.fork_of(project2.reload) expect(current_path).to eq(project_new_merge_request_path(fork)) @@ -130,5 +131,34 @@ describe 'User edits files' do expect(page).to have_content('New commit message') end + + context 'when the user already had a fork of the project', :js do + let!(:forked_project) { fork_project(project2, user, namespace: user.namespace, repository: true) } + before do + visit(project2_tree_path_root_ref) + end + + it 'links to the forked project for editing' do + click_link('.gitignore') + find('.js-edit-blob').click + + expect(page).not_to have_link('Fork') + expect(page).not_to have_button('Cancel') + + execute_script("ace.edit('editor').setValue('*.rbca')") + fill_in(:commit_message, with: 'Another commit', visible: true) + click_button('Commit changes') + + fork = user.fork_of(project2) + + expect(current_path).to eq(project_new_merge_request_path(fork)) + + wait_for_requests + + expect(page).to have_content('Another commit') + expect(page).to have_content("From #{forked_project.full_path}") + expect(page).to have_content("into #{project2.full_path}") + end + end end end diff --git a/spec/features/projects/user_replaces_files_spec.rb b/spec/features/projects/user_replaces_files_spec.rb index e284fdefd4f..a9628198d5b 100644 --- a/spec/features/projects/user_replaces_files_spec.rb +++ b/spec/features/projects/user_replaces_files_spec.rb @@ -74,7 +74,7 @@ describe 'User replaces files' do expect(page).to have_content('Replacement file commit message') - fork = user.fork_of(project2) + fork = user.fork_of(project2.reload) expect(current_path).to eq(project_new_merge_request_path(fork)) diff --git a/spec/features/projects/user_uploads_files_spec.rb b/spec/features/projects/user_uploads_files_spec.rb index 98871317ca3..8014c299980 100644 --- a/spec/features/projects/user_uploads_files_spec.rb +++ b/spec/features/projects/user_uploads_files_spec.rb @@ -39,6 +39,9 @@ describe 'User uploads files' do expect(current_path).to eq(project_new_merge_request_path(project)) click_link('Changes') + find("a[data-action='diffs']", text: 'Changes').click + + wait_for_requests expect(page).to have_content('Lorem ipsum dolor sit amet') expect(page).to have_content('Sed ut perspiciatis unde omnis') @@ -51,7 +54,7 @@ describe 'User uploads files' do visit(project2_tree_path_root_ref) end - it 'uploads and commit a new fileto a forked project', js: true do + it 'uploads and commit a new file to a forked project', js: true do find('.add-to-tree').click click_link('Upload file') @@ -69,11 +72,13 @@ describe 'User uploads files' do expect(page).to have_content('New commit message') - fork = user.fork_of(project2) + fork = user.fork_of(project2.reload) expect(current_path).to eq(project_new_merge_request_path(fork)) - click_link('Changes') + find("a[data-action='diffs']", text: 'Changes').click + + wait_for_requests expect(page).to have_content('Lorem ipsum dolor sit amet') expect(page).to have_content('Sed ut perspiciatis unde omnis') diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index 81f7ab80a04..4b2c54d54b5 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' feature 'Project' do + include ProjectForksHelper + describe 'creating from template' do let(:user) { create(:user) } let(:template) { Gitlab::ProjectTemplate.find(:rails) } @@ -57,11 +59,10 @@ feature 'Project' do describe 'remove forked relationship', js: true do let(:user) { create(:user) } - let(:project) { create(:project, namespace: user.namespace) } + let(:project) { fork_project(create(:project, :public), user, namespace_id: user.namespace) } before do sign_in user - create(:forked_project_link, forked_to_project: project) visit edit_project_path(project) end @@ -71,11 +72,60 @@ feature 'Project' do remove_with_confirm('Remove fork relationship', project.path) expect(page).to have_content 'The fork relationship has been removed.' - expect(project.forked?).to be_falsey + expect(project.reload.forked?).to be_falsey expect(page).not_to have_content 'Remove fork relationship' end end + describe 'showing information about source of a project fork' do + let(:user) { create(:user) } + let(:base_project) { create(:project, :public, :repository) } + let(:forked_project) { fork_project(base_project, user, repository: true) } + + before do + sign_in user + end + + it 'shows a link to the source project when it is available' do + visit project_path(forked_project) + + expect(page).to have_content('Forked from') + expect(page).to have_link(base_project.full_name) + end + + it 'does not contain fork network information for the root project' do + forked_project + + visit project_path(base_project) + + expect(page).not_to have_content('In fork network of') + expect(page).not_to have_content('Forked from') + end + + it 'shows the name of the deleted project when the source was deleted' do + forked_project + Projects::DestroyService.new(base_project, base_project.owner).execute + + visit project_path(forked_project) + + expect(page).to have_content("Forked from #{base_project.full_name} (deleted)") + end + + context 'a fork of a fork' do + let(:fork_of_fork) { fork_project(forked_project, user, repository: true) } + + it 'links to the base project if the source project is removed' do + fork_of_fork + Projects::DestroyService.new(forked_project, user).execute + + visit project_path(fork_of_fork) + + expect(page).to have_content("Forked from") + expect(page).to have_link(base_project.full_name) + end + end + end + describe 'removal', js: true do let(:user) { create(:user, username: 'test', name: 'test') } let(:project) { create(:project, namespace: user.namespace, name: 'project1') } diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb index aaf3d6d28ca..2ab1eda90f1 100644 --- a/spec/features/protected_branches_spec.rb +++ b/spec/features/protected_branches_spec.rb @@ -64,16 +64,24 @@ feature 'Protected Branches', :js do describe "Saved defaults" do it "keeps the allowed to merge and push dropdowns defaults based on the previous selection" do visit project_protected_branches_path(project) - find(".js-allowed-to-merge").trigger('click') - click_link 'No one' - find(".js-allowed-to-push").trigger('click') - click_link 'Developers + Masters' - visit project_protected_branches_path(project) - page.within(".js-allowed-to-merge") do - expect(page.find(".dropdown-toggle-text")).to have_content("No one") + form = '.js-new-protected-branch' + + within form do + find(".js-allowed-to-merge").trigger('click') + click_link 'No one' + find(".js-allowed-to-push").trigger('click') + click_link 'Developers + Masters' end - page.within(".js-allowed-to-push") do - expect(page.find(".dropdown-toggle-text")).to have_content("Developers + Masters") + + visit project_protected_branches_path(project) + + within form do + page.within(".js-allowed-to-merge") do + expect(page.find(".dropdown-toggle-text")).to have_content("No one") + end + page.within(".js-allowed-to-push") do + expect(page.find(".dropdown-toggle-text")).to have_content("Developers + Masters") + end end end end diff --git a/spec/finders/merge_request_target_project_finder_spec.rb b/spec/finders/merge_request_target_project_finder_spec.rb new file mode 100644 index 00000000000..c81bfd7932c --- /dev/null +++ b/spec/finders/merge_request_target_project_finder_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe MergeRequestTargetProjectFinder do + include ProjectForksHelper + + let(:user) { create(:user) } + subject(:finder) { described_class.new(current_user: user, source_project: forked_project) } + + shared_examples 'finding related projects' do + it 'finds sibling projects and base project' do + other_fork + + expect(finder.execute).to contain_exactly(base_project, other_fork, forked_project) + end + + it 'does not include projects that have merge requests turned off' do + other_fork.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED) + base_project.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED) + + expect(finder.execute).to contain_exactly(forked_project) + end + end + + context 'public projects' do + let(:base_project) { create(:project, :public, path: 'base') } + let(:forked_project) { fork_project(base_project) } + let(:other_fork) { fork_project(base_project) } + + it_behaves_like 'finding related projects' + end + + context 'private projects' do + let(:base_project) { create(:project, :private, path: 'base') } + let(:forked_project) { fork_project(base_project, base_project.owner) } + let(:other_fork) { fork_project(base_project, base_project.owner) } + + context 'when the user is a member of all projects' do + before do + base_project.add_developer(user) + forked_project.add_developer(user) + other_fork.add_developer(user) + end + + it_behaves_like 'finding related projects' + end + + it 'only finds the projects the user is a member of' do + other_fork.add_developer(user) + base_project.add_developer(user) + + expect(finder.execute).to contain_exactly(other_fork, base_project) + end + end +end diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index 95f445e7905..883bdf3746a 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -1,12 +1,18 @@ require 'spec_helper' describe MergeRequestsFinder do + include ProjectForksHelper + let(:user) { create :user } let(:user2) { create :user } - let(:project1) { create(:project) } - let(:project2) { create(:project, forked_from_project: project1) } - let(:project3) { create(:project, :archived, forked_from_project: project1) } + let(:project1) { create(:project, :public) } + let(:project2) { fork_project(project1, user) } + let(:project3) do + p = fork_project(project1, user) + p.update!(archived: true) + p + end let!(:merge_request1) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1) } let!(:merge_request2) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1, state: 'closed') } diff --git a/spec/fixtures/trace/trace_with_sections b/spec/fixtures/trace/trace_with_sections new file mode 100644 index 00000000000..21dff3928c3 --- /dev/null +++ b/spec/fixtures/trace/trace_with_sections @@ -0,0 +1,15 @@ +[0KRunning with gitlab-runner dev (HEAD) + on kitsune minikube (a21b584f) +[0;m[0;33mWARNING: Namespace is empty, therefore assuming 'default'. +[0;m[0KUsing Kubernetes namespace: default +[0;m[0KUsing Kubernetes executor with image alpine:3.4 ... +[0;msection_start:1506004954:prepare_script
[0KWaiting for pod default/runner-a21b584f-project-1208199-concurrent-0sg03f to be running, status is Pending +Running on runner-a21b584f-project-1208199-concurrent-0sg03f via kitsune.local... +section_end:1506004957:prepare_script
[0Ksection_start:1506004957:get_sources
[0K[32;1mCloning repository...[0;m +Cloning into '/nolith/ci-tests'... +[32;1mChecking out dddd7a6e as master...[0;m +[32;1mSkipping Git submodules setup[0;m +section_end:1506004958:get_sources
[0Ksection_start:1506004958:restore_cache
[0Ksection_end:1506004958:restore_cache
[0Ksection_start:1506004958:download_artifacts
[0Ksection_end:1506004958:download_artifacts
[0Ksection_start:1506004958:build_script
[0K[32;1m$ whoami[0;m +root +section_end:1506004959:build_script
[0Ksection_start:1506004959:after_script
[0Ksection_end:1506004959:after_script
[0Ksection_start:1506004959:archive_cache
[0Ksection_end:1506004959:archive_cache
[0Ksection_start:1506004959:upload_artifacts
[0Ksection_end:1506004959:upload_artifacts
[0K[32;1mJob succeeded +[0;m diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb index 7d1c17909bf..fd7900c32f4 100644 --- a/spec/helpers/merge_requests_helper_spec.rb +++ b/spec/helpers/merge_requests_helper_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe MergeRequestsHelper do + include ProjectForksHelper describe 'ci_build_details_path' do let(:project) { create(:project) } let(:merge_request) { MergeRequest.new } @@ -31,10 +32,10 @@ describe MergeRequestsHelper do describe 'within different projects' do let(:project) { create(:project) } - let(:fork_project) { create(:project, forked_from_project: project) } - let(:merge_request) { create(:merge_request, source_project: fork_project, target_project: project) } + let(:forked_project) { fork_project(project) } + let(:merge_request) { create(:merge_request, source_project: forked_project, target_project: project) } subject { format_mr_branch_names(merge_request) } - let(:source_title) { "#{fork_project.full_path}:#{merge_request.source_branch}" } + let(:source_title) { "#{forked_project.full_path}:#{merge_request.source_branch}" } let(:target_title) { "#{project.full_path}:#{merge_request.target_branch}" } it { is_expected.to eq([source_title, target_title]) } diff --git a/spec/javascripts/image_diff/helpers/badge_helper_spec.js b/spec/javascripts/image_diff/helpers/badge_helper_spec.js new file mode 100644 index 00000000000..fb9c7e59031 --- /dev/null +++ b/spec/javascripts/image_diff/helpers/badge_helper_spec.js @@ -0,0 +1,132 @@ +import * as badgeHelper from '~/image_diff/helpers/badge_helper'; +import * as mockData from '../mock_data'; + +describe('badge helper', () => { + const { coordinate, noteId, badgeText, badgeNumber } = mockData; + let containerEl; + let buttonEl; + + beforeEach(() => { + containerEl = document.createElement('div'); + }); + + describe('createImageBadge', () => { + beforeEach(() => { + buttonEl = badgeHelper.createImageBadge(noteId, coordinate); + }); + + it('should create button', () => { + expect(buttonEl.tagName).toEqual('BUTTON'); + expect(buttonEl.getAttribute('type')).toEqual('button'); + }); + + it('should set disabled attribute', () => { + expect(buttonEl.hasAttribute('disabled')).toEqual(true); + }); + + it('should set noteId', () => { + expect(buttonEl.dataset.noteId).toEqual(noteId); + }); + + it('should set coordinate', () => { + expect(buttonEl.style.left).toEqual(`${coordinate.x}px`); + expect(buttonEl.style.top).toEqual(`${coordinate.y}px`); + }); + + describe('classNames', () => { + it('should set .js-image-badge by default', () => { + expect(buttonEl.className).toEqual('js-image-badge'); + }); + + it('should add additional class names if parameter is passed', () => { + const classNames = ['first-class', 'second-class']; + buttonEl = badgeHelper.createImageBadge(noteId, coordinate, classNames); + + expect(buttonEl.className).toEqual(classNames.concat('js-image-badge').join(' ')); + }); + }); + }); + + describe('addImageBadge', () => { + beforeEach(() => { + badgeHelper.addImageBadge(containerEl, { + coordinate, + badgeText, + noteId, + }); + buttonEl = containerEl.querySelector('button'); + }); + + it('should appends button to container', () => { + expect(buttonEl).toBeDefined(); + }); + + it('should set the badge text', () => { + expect(buttonEl.innerText).toEqual(badgeText); + }); + + it('should set the button coordinates', () => { + expect(buttonEl.style.left).toEqual(`${coordinate.x}px`); + expect(buttonEl.style.top).toEqual(`${coordinate.y}px`); + }); + + it('should set the button noteId', () => { + expect(buttonEl.dataset.noteId).toEqual(noteId); + }); + }); + + describe('addImageCommentBadge', () => { + beforeEach(() => { + badgeHelper.addImageCommentBadge(containerEl, { + coordinate, + noteId, + }); + buttonEl = containerEl.querySelector('button'); + }); + + it('should append icon button to container', () => { + expect(buttonEl).toBeDefined(); + }); + + it('should create icon comment button', () => { + const iconEl = buttonEl.querySelector('i'); + expect(iconEl).toBeDefined(); + expect(iconEl.classList.contains('fa')).toEqual(true); + expect(iconEl.classList.contains('fa-comment-o')).toEqual(true); + }); + + it('should have .image-comment-badge.inverted in button class', () => { + expect(buttonEl.classList.contains('image-comment-badge')).toEqual(true); + expect(buttonEl.classList.contains('inverted')).toEqual(true); + }); + }); + + describe('addAvatarBadge', () => { + let avatarBadgeEl; + + beforeEach(() => { + containerEl.innerHTML = ` + <div id="${noteId}"> + <div class="badge hidden"> + </div> + </div> + `; + + badgeHelper.addAvatarBadge(containerEl, { + detail: { + noteId, + badgeNumber, + }, + }); + avatarBadgeEl = containerEl.querySelector(`#${noteId} .badge`); + }); + + it('should update badge number', () => { + expect(avatarBadgeEl.innerText).toEqual(badgeNumber.toString()); + }); + + it('should remove hidden class', () => { + expect(avatarBadgeEl.classList.contains('hidden')).toEqual(false); + }); + }); +}); diff --git a/spec/javascripts/image_diff/helpers/comment_indicator_helper_spec.js b/spec/javascripts/image_diff/helpers/comment_indicator_helper_spec.js new file mode 100644 index 00000000000..a284b981d2a --- /dev/null +++ b/spec/javascripts/image_diff/helpers/comment_indicator_helper_spec.js @@ -0,0 +1,139 @@ +import * as commentIndicatorHelper from '~/image_diff/helpers/comment_indicator_helper'; +import * as mockData from '../mock_data'; + +describe('commentIndicatorHelper', () => { + const { coordinate } = mockData; + let containerEl; + + beforeEach(() => { + containerEl = document.createElement('div'); + }); + + describe('addCommentIndicator', () => { + let buttonEl; + + beforeEach(() => { + commentIndicatorHelper.addCommentIndicator(containerEl, coordinate); + buttonEl = containerEl.querySelector('button'); + }); + + it('should append button to container', () => { + expect(buttonEl).toBeDefined(); + }); + + describe('button', () => { + it('should set coordinate', () => { + expect(buttonEl.style.left).toEqual(`${coordinate.x}px`); + expect(buttonEl.style.top).toEqual(`${coordinate.y}px`); + }); + + it('should contain image-comment-dark svg', () => { + const svgEl = buttonEl.querySelector('svg'); + expect(svgEl).toBeDefined(); + + const svgLink = svgEl.querySelector('use').getAttribute('xlink:href'); + expect(svgLink.indexOf('image-comment-dark') !== -1).toEqual(true); + }); + }); + }); + + describe('removeCommentIndicator', () => { + it('should return removed false if there is no comment-indicator', () => { + const result = commentIndicatorHelper.removeCommentIndicator(containerEl); + expect(result.removed).toEqual(false); + }); + + describe('has comment indicator', () => { + let result; + + beforeEach(() => { + containerEl.innerHTML = ` + <div class="comment-indicator" style="left:${coordinate.x}px; top: ${coordinate.y}px;"> + <img src="${gl.TEST_HOST}/image.png"> + </div> + `; + result = commentIndicatorHelper.removeCommentIndicator(containerEl); + }); + + it('should remove comment indicator', () => { + expect(containerEl.querySelector('.comment-indicator')).toBeNull(); + }); + + it('should return removed true', () => { + expect(result.removed).toEqual(true); + }); + + it('should return indicator meta', () => { + expect(result.x).toEqual(coordinate.x); + expect(result.y).toEqual(coordinate.y); + expect(result.image).toBeDefined(); + expect(result.image.width).toBeDefined(); + expect(result.image.height).toBeDefined(); + }); + }); + }); + + describe('showCommentIndicator', () => { + describe('commentIndicator exists', () => { + beforeEach(() => { + containerEl.innerHTML = ` + <button class="comment-indicator"></button> + `; + commentIndicatorHelper.showCommentIndicator(containerEl, coordinate); + }); + + it('should set commentIndicator coordinates', () => { + const commentIndicatorEl = containerEl.querySelector('.comment-indicator'); + expect(commentIndicatorEl.style.left).toEqual(`${coordinate.x}px`); + expect(commentIndicatorEl.style.top).toEqual(`${coordinate.y}px`); + }); + }); + + describe('commentIndicator does not exist', () => { + beforeEach(() => { + commentIndicatorHelper.showCommentIndicator(containerEl, coordinate); + }); + + it('should addCommentIndicator', () => { + const buttonEl = containerEl.querySelector('.comment-indicator'); + expect(buttonEl).toBeDefined(); + expect(buttonEl.style.left).toEqual(`${coordinate.x}px`); + expect(buttonEl.style.top).toEqual(`${coordinate.y}px`); + }); + }); + }); + + describe('commentIndicatorOnClick', () => { + let event; + let textAreaEl; + + beforeEach(() => { + containerEl.innerHTML = ` + <div class="diff-viewer"> + <button></button> + <div class="note-container"> + <textarea class="note-textarea"></textarea> + </div> + </div> + `; + textAreaEl = containerEl.querySelector('textarea'); + + event = { + stopPropagation: () => {}, + currentTarget: containerEl.querySelector('button'), + }; + + spyOn(event, 'stopPropagation'); + spyOn(textAreaEl, 'focus'); + commentIndicatorHelper.commentIndicatorOnClick(event); + }); + + it('should stopPropagation', () => { + expect(event.stopPropagation).toHaveBeenCalled(); + }); + + it('should focus textAreaEl', () => { + expect(textAreaEl.focus).toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/javascripts/image_diff/helpers/dom_helper_spec.js b/spec/javascripts/image_diff/helpers/dom_helper_spec.js new file mode 100644 index 00000000000..8dde924e8ae --- /dev/null +++ b/spec/javascripts/image_diff/helpers/dom_helper_spec.js @@ -0,0 +1,118 @@ +import * as domHelper from '~/image_diff/helpers/dom_helper'; +import * as mockData from '../mock_data'; + +describe('domHelper', () => { + const { imageMeta, badgeNumber } = mockData; + + describe('setPositionDataAttribute', () => { + let containerEl; + let attributeAfterCall; + const position = { + myProperty: 'myProperty', + }; + + beforeEach(() => { + containerEl = document.createElement('div'); + containerEl.dataset.position = JSON.stringify(position); + domHelper.setPositionDataAttribute(containerEl, imageMeta); + attributeAfterCall = JSON.parse(containerEl.dataset.position); + }); + + it('should set x, y, width, height', () => { + expect(attributeAfterCall.x).toEqual(imageMeta.x); + expect(attributeAfterCall.y).toEqual(imageMeta.y); + expect(attributeAfterCall.width).toEqual(imageMeta.width); + expect(attributeAfterCall.height).toEqual(imageMeta.height); + }); + + it('should not override other properties', () => { + expect(attributeAfterCall.myProperty).toEqual('myProperty'); + }); + }); + + describe('updateDiscussionAvatarBadgeNumber', () => { + let discussionEl; + + beforeEach(() => { + discussionEl = document.createElement('div'); + discussionEl.innerHTML = ` + <a href="#" class="image-diff-avatar-link"> + <div class="badge"></div> + </a> + `; + domHelper.updateDiscussionAvatarBadgeNumber(discussionEl, badgeNumber); + }); + + it('should update avatar badge number', () => { + expect(discussionEl.querySelector('.badge').innerText).toEqual(badgeNumber.toString()); + }); + }); + + describe('updateDiscussionBadgeNumber', () => { + let discussionEl; + + beforeEach(() => { + discussionEl = document.createElement('div'); + discussionEl.innerHTML = ` + <div class="badge"></div> + `; + domHelper.updateDiscussionBadgeNumber(discussionEl, badgeNumber); + }); + + it('should update discussion badge number', () => { + expect(discussionEl.querySelector('.badge').innerText).toEqual(badgeNumber.toString()); + }); + }); + + describe('toggleCollapsed', () => { + let element; + let discussionNotesEl; + + beforeEach(() => { + element = document.createElement('div'); + element.innerHTML = ` + <div class="discussion-notes"> + <button></button> + <form class="discussion-form"></form> + </div> + `; + discussionNotesEl = element.querySelector('.discussion-notes'); + }); + + describe('not collapsed', () => { + beforeEach(() => { + domHelper.toggleCollapsed({ + currentTarget: element.querySelector('button'), + }); + }); + + it('should add collapsed class', () => { + expect(discussionNotesEl.classList.contains('collapsed')).toEqual(true); + }); + + it('should force formEl to display none', () => { + const formEl = element.querySelector('.discussion-form'); + expect(formEl.style.display).toEqual('none'); + }); + }); + + describe('collapsed', () => { + beforeEach(() => { + discussionNotesEl.classList.add('collapsed'); + + domHelper.toggleCollapsed({ + currentTarget: element.querySelector('button'), + }); + }); + + it('should remove collapsed class', () => { + expect(discussionNotesEl.classList.contains('collapsed')).toEqual(false); + }); + + it('should force formEl to display block', () => { + const formEl = element.querySelector('.discussion-form'); + expect(formEl.style.display).toEqual('block'); + }); + }); + }); +}); diff --git a/spec/javascripts/image_diff/helpers/utils_helper_spec.js b/spec/javascripts/image_diff/helpers/utils_helper_spec.js new file mode 100644 index 00000000000..56d77a05c4c --- /dev/null +++ b/spec/javascripts/image_diff/helpers/utils_helper_spec.js @@ -0,0 +1,207 @@ +import * as utilsHelper from '~/image_diff/helpers/utils_helper'; +import ImageDiff from '~/image_diff/image_diff'; +import ReplacedImageDiff from '~/image_diff/replaced_image_diff'; +import ImageBadge from '~/image_diff/image_badge'; +import * as mockData from '../mock_data'; + +describe('utilsHelper', () => { + const { + noteId, + discussionId, + image, + imageProperties, + imageMeta, + } = mockData; + + describe('resizeCoordinatesToImageElement', () => { + let result; + + beforeEach(() => { + result = utilsHelper.resizeCoordinatesToImageElement(image, imageMeta); + }); + + it('should return x based on widthRatio', () => { + expect(result.x).toEqual(imageMeta.x * 0.5); + }); + + it('should return y based on heightRatio', () => { + expect(result.y).toEqual(imageMeta.y * 0.5); + }); + + it('should return image width', () => { + expect(result.width).toEqual(image.width); + }); + + it('should return image height', () => { + expect(result.height).toEqual(image.height); + }); + }); + + describe('generateBadgeFromDiscussionDOM', () => { + let discussionEl; + let result; + + beforeEach(() => { + const imageFrameEl = document.createElement('div'); + imageFrameEl.innerHTML = ` + <img src="${gl.TEST_HOST}/image.png"> + `; + discussionEl = document.createElement('div'); + discussionEl.dataset.discussionId = discussionId; + discussionEl.innerHTML = ` + <div class="note" id="${noteId}"></div> + `; + discussionEl.dataset.position = JSON.stringify(imageMeta); + result = utilsHelper.generateBadgeFromDiscussionDOM(imageFrameEl, discussionEl); + }); + + it('should return actual image properties', () => { + const { actual } = result; + expect(actual.x).toEqual(imageMeta.x); + expect(actual.y).toEqual(imageMeta.y); + expect(actual.width).toEqual(imageMeta.width); + expect(actual.height).toEqual(imageMeta.height); + }); + + it('should return browser image properties', () => { + const { browser } = result; + expect(browser.x).toBeDefined(); + expect(browser.y).toBeDefined(); + expect(browser.width).toBeDefined(); + expect(browser.height).toBeDefined(); + }); + + it('should return instance of ImageBadge', () => { + expect(result instanceof ImageBadge).toEqual(true); + }); + + it('should return noteId', () => { + expect(result.noteId).toEqual(noteId); + }); + + it('should return discussionId', () => { + expect(result.discussionId).toEqual(discussionId); + }); + }); + + describe('getTargetSelection', () => { + let containerEl; + + beforeEach(() => { + containerEl = { + querySelector: () => imageProperties, + }; + }); + + function generateEvent(offsetX, offsetY) { + return { + currentTarget: containerEl, + offsetX, + offsetY, + }; + } + + it('should return browser properties', () => { + const event = generateEvent(25, 25); + const result = utilsHelper.getTargetSelection(event); + + const { browser } = result; + expect(browser.x).toEqual(event.offsetX); + expect(browser.y).toEqual(event.offsetY); + expect(browser.width).toEqual(imageProperties.width); + expect(browser.height).toEqual(imageProperties.height); + }); + + it('should return resized actual image properties', () => { + const event = generateEvent(50, 50); + const result = utilsHelper.getTargetSelection(event); + + const { actual } = result; + expect(actual.x).toEqual(100); + expect(actual.y).toEqual(100); + expect(actual.width).toEqual(imageProperties.naturalWidth); + expect(actual.height).toEqual(imageProperties.naturalHeight); + }); + + describe('normalize coordinates', () => { + it('should return x = 0 if x < 0', () => { + const event = generateEvent(-5, 50); + const result = utilsHelper.getTargetSelection(event); + expect(result.browser.x).toEqual(0); + }); + + it('should return x = width if x > width', () => { + const event = generateEvent(1000, 50); + const result = utilsHelper.getTargetSelection(event); + expect(result.browser.x).toEqual(imageProperties.width); + }); + + it('should return y = 0 if y < 0', () => { + const event = generateEvent(50, -10); + const result = utilsHelper.getTargetSelection(event); + expect(result.browser.y).toEqual(0); + }); + + it('should return y = height if y > height', () => { + const event = generateEvent(50, 1000); + const result = utilsHelper.getTargetSelection(event); + expect(result.browser.y).toEqual(imageProperties.height); + }); + }); + }); + + describe('initImageDiff', () => { + let glCache; + let fileEl; + + beforeEach(() => { + window.gl = window.gl || (window.gl = {}); + glCache = window.gl; + window.gl.ImageFile = () => {}; + fileEl = document.createElement('div'); + fileEl.innerHTML = ` + <div class="diff-file"></div> + `; + + spyOn(ImageDiff.prototype, 'init').and.callFake(() => {}); + spyOn(ReplacedImageDiff.prototype, 'init').and.callFake(() => {}); + }); + + afterEach(() => { + window.gl = glCache; + }); + + it('should initialize gl.ImageFile', () => { + spyOn(window.gl, 'ImageFile'); + + utilsHelper.initImageDiff(fileEl, false, false); + expect(gl.ImageFile).toHaveBeenCalled(); + }); + + it('should initialize ImageDiff if js-single-image', () => { + const diffFileEl = fileEl.querySelector('.diff-file'); + diffFileEl.innerHTML = ` + <div class="js-single-image"> + </div> + `; + + const imageDiff = utilsHelper.initImageDiff(fileEl, true, false); + expect(ImageDiff.prototype.init).toHaveBeenCalled(); + expect(imageDiff.canCreateNote).toEqual(true); + expect(imageDiff.renderCommentBadge).toEqual(false); + }); + + it('should initialize ReplacedImageDiff if js-replaced-image', () => { + const diffFileEl = fileEl.querySelector('.diff-file'); + diffFileEl.innerHTML = ` + <div class="js-replaced-image"> + </div> + `; + + const replacedImageDiff = utilsHelper.initImageDiff(fileEl, false, true); + expect(ReplacedImageDiff.prototype.init).toHaveBeenCalled(); + expect(replacedImageDiff.canCreateNote).toEqual(false); + expect(replacedImageDiff.renderCommentBadge).toEqual(true); + }); + }); +}); diff --git a/spec/javascripts/image_diff/image_badge_spec.js b/spec/javascripts/image_diff/image_badge_spec.js new file mode 100644 index 00000000000..87f98fc0926 --- /dev/null +++ b/spec/javascripts/image_diff/image_badge_spec.js @@ -0,0 +1,84 @@ +import ImageBadge from '~/image_diff/image_badge'; +import imageDiffHelper from '~/image_diff/helpers/index'; +import * as mockData from './mock_data'; + +describe('ImageBadge', () => { + const { noteId, discussionId, imageMeta } = mockData; + const options = { + noteId, + discussionId, + }; + + it('should save actual property', () => { + const imageBadge = new ImageBadge(Object.assign({}, options, { + actual: imageMeta, + })); + + const { actual } = imageBadge; + expect(actual.x).toEqual(imageMeta.x); + expect(actual.y).toEqual(imageMeta.y); + expect(actual.width).toEqual(imageMeta.width); + expect(actual.height).toEqual(imageMeta.height); + }); + + it('should save browser property', () => { + const imageBadge = new ImageBadge(Object.assign({}, options, { + browser: imageMeta, + })); + + const { browser } = imageBadge; + expect(browser.x).toEqual(imageMeta.x); + expect(browser.y).toEqual(imageMeta.y); + expect(browser.width).toEqual(imageMeta.width); + expect(browser.height).toEqual(imageMeta.height); + }); + + it('should save noteId', () => { + const imageBadge = new ImageBadge(options); + expect(imageBadge.noteId).toEqual(noteId); + }); + + it('should save discussionId', () => { + const imageBadge = new ImageBadge(options); + expect(imageBadge.discussionId).toEqual(discussionId); + }); + + describe('default values', () => { + let imageBadge; + + beforeEach(() => { + imageBadge = new ImageBadge(options); + }); + + it('should return defaultimageMeta if actual property is not provided', () => { + const { actual } = imageBadge; + expect(actual.x).toEqual(0); + expect(actual.y).toEqual(0); + expect(actual.width).toEqual(0); + expect(actual.height).toEqual(0); + }); + + it('should return defaultimageMeta if browser property is not provided', () => { + const { browser } = imageBadge; + expect(browser.x).toEqual(0); + expect(browser.y).toEqual(0); + expect(browser.width).toEqual(0); + expect(browser.height).toEqual(0); + }); + }); + + describe('imageEl property is provided and not browser property', () => { + beforeEach(() => { + spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement').and.returnValue(true); + }); + + it('should generate browser property', () => { + const imageBadge = new ImageBadge(Object.assign({}, options, { + imageEl: document.createElement('img'), + })); + + expect(imageDiffHelper.resizeCoordinatesToImageElement).toHaveBeenCalled(); + expect(imageBadge.browser).toEqual(true); + }); + }); +}); diff --git a/spec/javascripts/image_diff/image_diff_spec.js b/spec/javascripts/image_diff/image_diff_spec.js new file mode 100644 index 00000000000..346282328c7 --- /dev/null +++ b/spec/javascripts/image_diff/image_diff_spec.js @@ -0,0 +1,361 @@ +import ImageDiff from '~/image_diff/image_diff'; +import * as imageUtility from '~/lib/utils/image_utility'; +import imageDiffHelper from '~/image_diff/helpers/index'; +import * as mockData from './mock_data'; + +describe('ImageDiff', () => { + let element; + let imageDiff; + + beforeEach(() => { + setFixtures(` + <div id="element"> + <div class="diff-file"> + <div class="js-image-frame"> + <img src="${gl.TEST_HOST}/image.png"> + <div class="comment-indicator"></div> + <div id="badge-1" class="badge">1</div> + <div id="badge-2" class="badge">2</div> + <div id="badge-3" class="badge">3</div> + </div> + <div class="note-container"> + <div class="discussion-notes"> + <div class="js-diff-notes-toggle"></div> + <div class="notes"></div> + </div> + <div class="discussion-notes"> + <div class="js-diff-notes-toggle"></div> + <div class="notes"></div> + </div> + </div> + </div> + </div> + `); + element = document.getElementById('element'); + }); + + describe('constructor', () => { + beforeEach(() => { + imageDiff = new ImageDiff(element, { + canCreateNote: true, + renderCommentBadge: true, + }); + }); + + it('should set el', () => { + expect(imageDiff.el).toEqual(element); + }); + + it('should set canCreateNote', () => { + expect(imageDiff.canCreateNote).toEqual(true); + }); + + it('should set renderCommentBadge', () => { + expect(imageDiff.renderCommentBadge).toEqual(true); + }); + + it('should set $noteContainer', () => { + expect(imageDiff.$noteContainer[0]).toEqual(element.querySelector('.note-container')); + }); + + describe('default', () => { + beforeEach(() => { + imageDiff = new ImageDiff(element); + }); + + it('should set canCreateNote as false', () => { + expect(imageDiff.canCreateNote).toEqual(false); + }); + + it('should set renderCommentBadge as false', () => { + expect(imageDiff.renderCommentBadge).toEqual(false); + }); + }); + }); + + describe('init', () => { + beforeEach(() => { + spyOn(ImageDiff.prototype, 'bindEvents').and.callFake(() => {}); + imageDiff = new ImageDiff(element); + imageDiff.init(); + }); + + it('should set imageFrameEl', () => { + expect(imageDiff.imageFrameEl).toEqual(element.querySelector('.diff-file .js-image-frame')); + }); + + it('should set imageEl', () => { + expect(imageDiff.imageEl).toEqual(element.querySelector('.diff-file .js-image-frame img')); + }); + + it('should call bindEvents', () => { + expect(imageDiff.bindEvents).toHaveBeenCalled(); + }); + }); + + describe('bindEvents', () => { + let imageEl; + + beforeEach(() => { + spyOn(imageDiffHelper, 'toggleCollapsed').and.callFake(() => {}); + spyOn(imageDiffHelper, 'commentIndicatorOnClick').and.callFake(() => {}); + spyOn(imageDiffHelper, 'removeCommentIndicator').and.callFake(() => {}); + spyOn(ImageDiff.prototype, 'imageClicked').and.callFake(() => {}); + spyOn(ImageDiff.prototype, 'addBadge').and.callFake(() => {}); + spyOn(ImageDiff.prototype, 'removeBadge').and.callFake(() => {}); + spyOn(ImageDiff.prototype, 'renderBadges').and.callFake(() => {}); + imageEl = element.querySelector('.diff-file .js-image-frame img'); + }); + + describe('default', () => { + beforeEach(() => { + spyOn(imageUtility, 'isImageLoaded').and.returnValue(false); + imageDiff = new ImageDiff(element); + imageDiff.imageEl = imageEl; + imageDiff.bindEvents(); + }); + + it('should register click event delegation to js-diff-notes-toggle', () => { + element.querySelector('.js-diff-notes-toggle').click(); + expect(imageDiffHelper.toggleCollapsed).toHaveBeenCalled(); + }); + + it('should register click event delegation to comment-indicator', () => { + element.querySelector('.comment-indicator').click(); + expect(imageDiffHelper.commentIndicatorOnClick).toHaveBeenCalled(); + }); + }); + + describe('image loaded', () => { + beforeEach(() => { + spyOn(imageUtility, 'isImageLoaded').and.returnValue(true); + imageDiff = new ImageDiff(element); + imageDiff.imageEl = imageEl; + }); + + it('should renderBadges', () => {}); + }); + + describe('image not loaded', () => { + beforeEach(() => { + spyOn(imageUtility, 'isImageLoaded').and.returnValue(false); + imageDiff = new ImageDiff(element); + imageDiff.imageEl = imageEl; + imageDiff.bindEvents(); + }); + + it('should registers load eventListener', () => { + const loadEvent = new Event('load'); + imageEl.dispatchEvent(loadEvent); + expect(imageDiff.renderBadges).toHaveBeenCalled(); + }); + }); + + describe('canCreateNote', () => { + beforeEach(() => { + spyOn(imageUtility, 'isImageLoaded').and.returnValue(false); + imageDiff = new ImageDiff(element, { + canCreateNote: true, + }); + imageDiff.imageEl = imageEl; + imageDiff.bindEvents(); + }); + + it('should register click.imageDiff event', () => { + const event = new CustomEvent('click.imageDiff'); + element.dispatchEvent(event); + expect(imageDiff.imageClicked).toHaveBeenCalled(); + }); + + it('should register blur.imageDiff event', () => { + const event = new CustomEvent('blur.imageDiff'); + element.dispatchEvent(event); + expect(imageDiffHelper.removeCommentIndicator).toHaveBeenCalled(); + }); + + it('should register addBadge.imageDiff event', () => { + const event = new CustomEvent('addBadge.imageDiff'); + element.dispatchEvent(event); + expect(imageDiff.addBadge).toHaveBeenCalled(); + }); + + it('should register removeBadge.imageDiff event', () => { + const event = new CustomEvent('removeBadge.imageDiff'); + element.dispatchEvent(event); + expect(imageDiff.removeBadge).toHaveBeenCalled(); + }); + }); + + describe('canCreateNote is false', () => { + beforeEach(() => { + spyOn(imageUtility, 'isImageLoaded').and.returnValue(false); + imageDiff = new ImageDiff(element); + imageDiff.imageEl = imageEl; + imageDiff.bindEvents(); + }); + + it('should not register click.imageDiff event', () => { + const event = new CustomEvent('click.imageDiff'); + element.dispatchEvent(event); + expect(imageDiff.imageClicked).not.toHaveBeenCalled(); + }); + }); + }); + + describe('imageClicked', () => { + beforeEach(() => { + spyOn(imageDiffHelper, 'getTargetSelection').and.returnValue({ + actual: {}, + browser: {}, + }); + spyOn(imageDiffHelper, 'setPositionDataAttribute').and.callFake(() => {}); + spyOn(imageDiffHelper, 'showCommentIndicator').and.callFake(() => {}); + imageDiff = new ImageDiff(element); + imageDiff.imageClicked({ + detail: { + currentTarget: {}, + }, + }); + }); + + it('should call getTargetSelection', () => { + expect(imageDiffHelper.getTargetSelection).toHaveBeenCalled(); + }); + + it('should call setPositionDataAttribute', () => { + expect(imageDiffHelper.setPositionDataAttribute).toHaveBeenCalled(); + }); + + it('should call showCommentIndicator', () => { + expect(imageDiffHelper.showCommentIndicator).toHaveBeenCalled(); + }); + }); + + describe('renderBadges', () => { + beforeEach(() => { + spyOn(ImageDiff.prototype, 'renderBadge').and.callFake(() => {}); + imageDiff = new ImageDiff(element); + imageDiff.renderBadges(); + }); + + it('should call renderBadge for each discussionEl', () => { + const discussionEls = element.querySelectorAll('.note-container .discussion-notes .notes'); + expect(imageDiff.renderBadge.calls.count()).toEqual(discussionEls.length); + }); + }); + + describe('renderBadge', () => { + let discussionEls; + + beforeEach(() => { + spyOn(imageDiffHelper, 'addImageBadge').and.callFake(() => {}); + spyOn(imageDiffHelper, 'addImageCommentBadge').and.callFake(() => {}); + spyOn(imageDiffHelper, 'generateBadgeFromDiscussionDOM').and.returnValue({ + browser: {}, + noteId: 'noteId', + }); + discussionEls = element.querySelectorAll('.note-container .discussion-notes .notes'); + imageDiff = new ImageDiff(element); + imageDiff.renderBadge(discussionEls[0], 0); + }); + + it('should populate imageBadges', () => { + expect(imageDiff.imageBadges.length).toEqual(1); + }); + + describe('renderCommentBadge', () => { + beforeEach(() => { + imageDiff.renderCommentBadge = true; + imageDiff.renderBadge(discussionEls[0], 0); + }); + + it('should call addImageCommentBadge', () => { + expect(imageDiffHelper.addImageCommentBadge).toHaveBeenCalled(); + }); + }); + + describe('renderCommentBadge is false', () => { + it('should call addImageBadge', () => { + expect(imageDiffHelper.addImageBadge).toHaveBeenCalled(); + }); + }); + }); + + describe('addBadge', () => { + beforeEach(() => { + spyOn(imageDiffHelper, 'addImageBadge').and.callFake(() => {}); + spyOn(imageDiffHelper, 'addAvatarBadge').and.callFake(() => {}); + spyOn(imageDiffHelper, 'updateDiscussionBadgeNumber').and.callFake(() => {}); + imageDiff = new ImageDiff(element); + imageDiff.imageFrameEl = element.querySelector('.diff-file .js-image-frame'); + imageDiff.addBadge({ + detail: { + x: 0, + y: 1, + width: 25, + height: 50, + noteId: 'noteId', + discussionId: 'discussionId', + }, + }); + }); + + it('should add imageBadge to imageBadges', () => { + expect(imageDiff.imageBadges.length).toEqual(1); + }); + + it('should call addImageBadge', () => { + expect(imageDiffHelper.addImageBadge).toHaveBeenCalled(); + }); + + it('should call addAvatarBadge', () => { + expect(imageDiffHelper.addAvatarBadge).toHaveBeenCalled(); + }); + + it('should call updateDiscussionBadgeNumber', () => { + expect(imageDiffHelper.updateDiscussionBadgeNumber).toHaveBeenCalled(); + }); + }); + + describe('removeBadge', () => { + beforeEach(() => { + const { imageMeta } = mockData; + + spyOn(imageDiffHelper, 'updateDiscussionBadgeNumber').and.callFake(() => {}); + spyOn(imageDiffHelper, 'updateDiscussionAvatarBadgeNumber').and.callFake(() => {}); + imageDiff = new ImageDiff(element); + imageDiff.imageBadges = [imageMeta, imageMeta, imageMeta]; + imageDiff.imageFrameEl = element.querySelector('.diff-file .js-image-frame'); + imageDiff.removeBadge({ + detail: { + badgeNumber: 2, + }, + }); + }); + + describe('cascade badge count', () => { + it('should update next imageBadgeEl value', () => { + const imageBadgeEls = imageDiff.imageFrameEl.querySelectorAll('.badge'); + expect(imageBadgeEls[0].innerText).toEqual('1'); + expect(imageBadgeEls[1].innerText).toEqual('2'); + expect(imageBadgeEls.length).toEqual(2); + }); + + it('should call updateDiscussionBadgeNumber', () => { + expect(imageDiffHelper.updateDiscussionBadgeNumber).toHaveBeenCalled(); + }); + + it('should call updateDiscussionAvatarBadgeNumber', () => { + expect(imageDiffHelper.updateDiscussionAvatarBadgeNumber).toHaveBeenCalled(); + }); + }); + + it('should remove badge from imageBadges', () => { + expect(imageDiff.imageBadges.length).toEqual(2); + }); + + it('should remove imageBadgeEl', () => { + expect(imageDiff.imageFrameEl.querySelector('#badge-2')).toBeNull(); + }); + }); +}); diff --git a/spec/javascripts/image_diff/init_discussion_tab_spec.js b/spec/javascripts/image_diff/init_discussion_tab_spec.js new file mode 100644 index 00000000000..7c447d6f70d --- /dev/null +++ b/spec/javascripts/image_diff/init_discussion_tab_spec.js @@ -0,0 +1,37 @@ +import initDiscussionTab from '~/image_diff/init_discussion_tab'; +import imageDiffHelper from '~/image_diff/helpers/index'; + +describe('initDiscussionTab', () => { + beforeEach(() => { + setFixtures(` + <div class="timeline-content"> + <div class="diff-file js-image-file"></div> + <div class="diff-file js-image-file"></div> + </div> + `); + }); + + it('should pass canCreateNote as false to initImageDiff', (done) => { + spyOn(imageDiffHelper, 'initImageDiff').and.callFake((diffFileEl, canCreateNote) => { + expect(canCreateNote).toEqual(false); + done(); + }); + + initDiscussionTab(); + }); + + it('should pass renderCommentBadge as true to initImageDiff', (done) => { + spyOn(imageDiffHelper, 'initImageDiff').and.callFake((diffFileEl, canCreateNote, renderCommentBadge) => { + expect(renderCommentBadge).toEqual(true); + done(); + }); + + initDiscussionTab(); + }); + + it('should call initImageDiff for each diffFileEls', () => { + spyOn(imageDiffHelper, 'initImageDiff').and.callFake(() => {}); + initDiscussionTab(); + expect(imageDiffHelper.initImageDiff.calls.count()).toEqual(2); + }); +}); diff --git a/spec/javascripts/image_diff/mock_data.js b/spec/javascripts/image_diff/mock_data.js new file mode 100644 index 00000000000..a0d1732dd0a --- /dev/null +++ b/spec/javascripts/image_diff/mock_data.js @@ -0,0 +1,28 @@ +export const noteId = 'noteId'; +export const discussionId = 'discussionId'; +export const badgeText = 'badgeText'; +export const badgeNumber = 5; + +export const coordinate = { + x: 100, + y: 100, +}; + +export const image = { + width: 100, + height: 100, +}; + +export const imageProperties = { + width: image.width, + height: image.height, + naturalWidth: image.width * 2, + naturalHeight: image.height * 2, +}; + +export const imageMeta = { + x: coordinate.x, + y: coordinate.y, + width: imageProperties.naturalWidth, + height: imageProperties.naturalHeight, +}; diff --git a/spec/javascripts/image_diff/replaced_image_diff_spec.js b/spec/javascripts/image_diff/replaced_image_diff_spec.js new file mode 100644 index 00000000000..5f8cd7c531a --- /dev/null +++ b/spec/javascripts/image_diff/replaced_image_diff_spec.js @@ -0,0 +1,312 @@ +import ReplacedImageDiff from '~/image_diff/replaced_image_diff'; +import ImageDiff from '~/image_diff/image_diff'; +import { viewTypes } from '~/image_diff/view_types'; +import imageDiffHelper from '~/image_diff/helpers/index'; + +describe('ReplacedImageDiff', () => { + let element; + let replacedImageDiff; + + beforeEach(() => { + setFixtures(` + <div id="element"> + <div class="two-up"> + <div class="js-image-frame"> + <img src="${gl.TEST_HOST}/image.png"> + </div> + </div> + <div class="swipe"> + <div class="js-image-frame"> + <img src="${gl.TEST_HOST}/image.png"> + </div> + </div> + <div class="onion-skin"> + <div class="js-image-frame"> + <img src="${gl.TEST_HOST}/image.png"> + </div> + </div> + <div class="view-modes-menu"> + <div class="two-up">2-up</div> + <div class="swipe">Swipe</div> + <div class="onion-skin">Onion skin</div> + </div> + </div> + `); + element = document.getElementById('element'); + }); + + function setupImageFrameEls() { + replacedImageDiff.imageFrameEls = []; + replacedImageDiff.imageFrameEls[viewTypes.TWO_UP] = element.querySelector('.two-up .js-image-frame'); + replacedImageDiff.imageFrameEls[viewTypes.SWIPE] = element.querySelector('.swipe .js-image-frame'); + replacedImageDiff.imageFrameEls[viewTypes.ONION_SKIN] = element.querySelector('.onion-skin .js-image-frame'); + } + + function setupViewModesEls() { + replacedImageDiff.viewModesEls = []; + replacedImageDiff.viewModesEls[viewTypes.TWO_UP] = element.querySelector('.view-modes-menu .two-up'); + replacedImageDiff.viewModesEls[viewTypes.SWIPE] = element.querySelector('.view-modes-menu .swipe'); + replacedImageDiff.viewModesEls[viewTypes.ONION_SKIN] = element.querySelector('.view-modes-menu .onion-skin'); + } + + function setupImageEls() { + replacedImageDiff.imageEls = []; + replacedImageDiff.imageEls[viewTypes.TWO_UP] = element.querySelector('.two-up img'); + replacedImageDiff.imageEls[viewTypes.SWIPE] = element.querySelector('.swipe img'); + replacedImageDiff.imageEls[viewTypes.ONION_SKIN] = element.querySelector('.onion-skin img'); + } + + it('should extend ImageDiff', () => { + replacedImageDiff = new ReplacedImageDiff(element); + expect(replacedImageDiff instanceof ImageDiff).toEqual(true); + }); + + describe('init', () => { + beforeEach(() => { + spyOn(ReplacedImageDiff.prototype, 'bindEvents').and.callFake(() => {}); + spyOn(ReplacedImageDiff.prototype, 'generateImageEls').and.callFake(() => {}); + + replacedImageDiff = new ReplacedImageDiff(element); + replacedImageDiff.init(); + }); + + it('should set imageFrameEls', () => { + const { imageFrameEls } = replacedImageDiff; + expect(imageFrameEls).toBeDefined(); + expect(imageFrameEls[viewTypes.TWO_UP]).toEqual(element.querySelector('.two-up .js-image-frame')); + expect(imageFrameEls[viewTypes.SWIPE]).toEqual(element.querySelector('.swipe .js-image-frame')); + expect(imageFrameEls[viewTypes.ONION_SKIN]).toEqual(element.querySelector('.onion-skin .js-image-frame')); + }); + + it('should set viewModesEls', () => { + const { viewModesEls } = replacedImageDiff; + expect(viewModesEls).toBeDefined(); + expect(viewModesEls[viewTypes.TWO_UP]).toEqual(element.querySelector('.view-modes-menu .two-up')); + expect(viewModesEls[viewTypes.SWIPE]).toEqual(element.querySelector('.view-modes-menu .swipe')); + expect(viewModesEls[viewTypes.ONION_SKIN]).toEqual(element.querySelector('.view-modes-menu .onion-skin')); + }); + + it('should generateImageEls', () => { + expect(ReplacedImageDiff.prototype.generateImageEls).toHaveBeenCalled(); + }); + + it('should bindEvents', () => { + expect(ReplacedImageDiff.prototype.bindEvents).toHaveBeenCalled(); + }); + + describe('currentView', () => { + it('should set currentView', () => { + replacedImageDiff.init(viewTypes.ONION_SKIN); + expect(replacedImageDiff.currentView).toEqual(viewTypes.ONION_SKIN); + }); + + it('should default to viewTypes.TWO_UP', () => { + expect(replacedImageDiff.currentView).toEqual(viewTypes.TWO_UP); + }); + }); + }); + + describe('generateImageEls', () => { + beforeEach(() => { + spyOn(ReplacedImageDiff.prototype, 'bindEvents').and.callFake(() => {}); + + replacedImageDiff = new ReplacedImageDiff(element, { + canCreateNote: false, + renderCommentBadge: false, + }); + + setupImageFrameEls(); + }); + + it('should set imageEls', () => { + replacedImageDiff.generateImageEls(); + const { imageEls } = replacedImageDiff; + expect(imageEls).toBeDefined(); + expect(imageEls[viewTypes.TWO_UP]).toEqual(element.querySelector('.two-up img')); + expect(imageEls[viewTypes.SWIPE]).toEqual(element.querySelector('.swipe img')); + expect(imageEls[viewTypes.ONION_SKIN]).toEqual(element.querySelector('.onion-skin img')); + }); + }); + + describe('bindEvents', () => { + beforeEach(() => { + spyOn(ImageDiff.prototype, 'bindEvents').and.callFake(() => {}); + replacedImageDiff = new ReplacedImageDiff(element); + + setupViewModesEls(); + }); + + it('should call super.bindEvents', () => { + replacedImageDiff.bindEvents(); + expect(ImageDiff.prototype.bindEvents).toHaveBeenCalled(); + }); + + it('should register click eventlistener to 2-up view mode', (done) => { + spyOn(ReplacedImageDiff.prototype, 'changeView').and.callFake((viewMode) => { + expect(viewMode).toEqual(viewTypes.TWO_UP); + done(); + }); + + replacedImageDiff.bindEvents(); + replacedImageDiff.viewModesEls[viewTypes.TWO_UP].click(); + }); + + it('should register click eventlistener to swipe view mode', (done) => { + spyOn(ReplacedImageDiff.prototype, 'changeView').and.callFake((viewMode) => { + expect(viewMode).toEqual(viewTypes.SWIPE); + done(); + }); + + replacedImageDiff.bindEvents(); + replacedImageDiff.viewModesEls[viewTypes.SWIPE].click(); + }); + + it('should register click eventlistener to onion skin view mode', (done) => { + spyOn(ReplacedImageDiff.prototype, 'changeView').and.callFake((viewMode) => { + expect(viewMode).toEqual(viewTypes.SWIPE); + done(); + }); + + replacedImageDiff.bindEvents(); + replacedImageDiff.viewModesEls[viewTypes.SWIPE].click(); + }); + }); + + describe('getters', () => { + describe('imageEl', () => { + beforeEach(() => { + replacedImageDiff = new ReplacedImageDiff(element); + replacedImageDiff.currentView = viewTypes.TWO_UP; + setupImageEls(); + }); + + it('should return imageEl based on currentView', () => { + expect(replacedImageDiff.imageEl).toEqual(element.querySelector('.two-up img')); + + replacedImageDiff.currentView = viewTypes.SWIPE; + expect(replacedImageDiff.imageEl).toEqual(element.querySelector('.swipe img')); + }); + }); + + describe('imageFrameEl', () => { + beforeEach(() => { + replacedImageDiff = new ReplacedImageDiff(element); + replacedImageDiff.currentView = viewTypes.TWO_UP; + setupImageFrameEls(); + }); + + it('should return imageFrameEl based on currentView', () => { + expect(replacedImageDiff.imageFrameEl).toEqual(element.querySelector('.two-up .js-image-frame')); + + replacedImageDiff.currentView = viewTypes.ONION_SKIN; + expect(replacedImageDiff.imageFrameEl).toEqual(element.querySelector('.onion-skin .js-image-frame')); + }); + }); + }); + + describe('changeView', () => { + beforeEach(() => { + replacedImageDiff = new ReplacedImageDiff(element); + spyOn(imageDiffHelper, 'removeCommentIndicator').and.returnValue({ + removed: false, + }); + setupImageFrameEls(); + }); + + describe('invalid viewType', () => { + beforeEach(() => { + replacedImageDiff.changeView('some-view-name'); + }); + + it('should not call removeCommentIndicator', () => { + expect(imageDiffHelper.removeCommentIndicator).not.toHaveBeenCalled(); + }); + }); + + describe('valid viewType', () => { + beforeEach(() => { + jasmine.clock().install(); + spyOn(ReplacedImageDiff.prototype, 'renderNewView').and.callFake(() => {}); + replacedImageDiff.changeView(viewTypes.ONION_SKIN); + }); + + afterEach(() => { + jasmine.clock().uninstall(); + }); + + it('should call removeCommentIndicator', () => { + expect(imageDiffHelper.removeCommentIndicator).toHaveBeenCalled(); + }); + + it('should update currentView to newView', () => { + expect(replacedImageDiff.currentView).toEqual(viewTypes.ONION_SKIN); + }); + + it('should clear imageBadges', () => { + expect(replacedImageDiff.imageBadges.length).toEqual(0); + }); + + it('should call renderNewView', () => { + jasmine.clock().tick(251); + expect(replacedImageDiff.renderNewView).toHaveBeenCalled(); + }); + }); + }); + + describe('renderNewView', () => { + beforeEach(() => { + replacedImageDiff = new ReplacedImageDiff(element); + }); + + it('should call renderBadges', () => { + spyOn(ReplacedImageDiff.prototype, 'renderBadges').and.callFake(() => {}); + + replacedImageDiff.renderNewView({ + removed: false, + }); + + expect(replacedImageDiff.renderBadges).toHaveBeenCalled(); + }); + + describe('removeIndicator', () => { + const indicator = { + removed: true, + x: 0, + y: 1, + image: { + width: 50, + height: 100, + }, + }; + + beforeEach(() => { + setupImageEls(); + setupImageFrameEls(); + }); + + it('should pass showCommentIndicator normalized indicator values', (done) => { + spyOn(imageDiffHelper, 'showCommentIndicator').and.callFake(() => {}); + spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement').and.callFake((imageEl, meta) => { + expect(meta.x).toEqual(indicator.x); + expect(meta.y).toEqual(indicator.y); + expect(meta.width).toEqual(indicator.image.width); + expect(meta.height).toEqual(indicator.image.height); + done(); + }); + replacedImageDiff.renderNewView(indicator); + }); + + it('should call showCommentIndicator', (done) => { + const normalized = { + normalized: true, + }; + spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement').and.returnValue(normalized); + spyOn(imageDiffHelper, 'showCommentIndicator').and.callFake((imageFrameEl, normalizedIndicator) => { + expect(normalizedIndicator).toEqual(normalized); + done(); + }); + replacedImageDiff.renderNewView(indicator); + }); + }); + }); +}); diff --git a/spec/javascripts/image_diff/view_types_spec.js b/spec/javascripts/image_diff/view_types_spec.js new file mode 100644 index 00000000000..e9639f46497 --- /dev/null +++ b/spec/javascripts/image_diff/view_types_spec.js @@ -0,0 +1,24 @@ +import { viewTypes, isValidViewType } from '~/image_diff/view_types'; + +describe('viewTypes', () => { + describe('isValidViewType', () => { + it('should return true for TWO_UP', () => { + expect(isValidViewType(viewTypes.TWO_UP)).toEqual(true); + }); + + it('should return true for SWIPE', () => { + expect(isValidViewType(viewTypes.SWIPE)).toEqual(true); + }); + + it('should return true for ONION_SKIN', () => { + expect(isValidViewType(viewTypes.ONION_SKIN)).toEqual(true); + }); + + it('should return false for non view types', () => { + expect(isValidViewType('some-view-type')).toEqual(false); + expect(isValidViewType(null)).toEqual(false); + expect(isValidViewType(undefined)).toEqual(false); + expect(isValidViewType('')).toEqual(false); + }); + }); +}); diff --git a/spec/javascripts/lib/utils/image_utility_spec.js b/spec/javascripts/lib/utils/image_utility_spec.js new file mode 100644 index 00000000000..75addfcc833 --- /dev/null +++ b/spec/javascripts/lib/utils/image_utility_spec.js @@ -0,0 +1,32 @@ +import * as imageUtility from '~/lib/utils/image_utility'; + +describe('imageUtility', () => { + describe('isImageLoaded', () => { + it('should return false when image.complete is false', () => { + const element = { + complete: false, + naturalHeight: 100, + }; + + expect(imageUtility.isImageLoaded(element)).toEqual(false); + }); + + it('should return false when naturalHeight = 0', () => { + const element = { + complete: true, + naturalHeight: 0, + }; + + expect(imageUtility.isImageLoaded(element)).toEqual(false); + }); + + it('should return true when image.complete and naturalHeight != 0', () => { + const element = { + complete: true, + naturalHeight: 100, + }; + + expect(imageUtility.isImageLoaded(element)).toEqual(true); + }); + }); +}); diff --git a/spec/javascripts/profile/account/components/delete_account_modal_spec.js b/spec/javascripts/profile/account/components/delete_account_modal_spec.js new file mode 100644 index 00000000000..2e94948cfb2 --- /dev/null +++ b/spec/javascripts/profile/account/components/delete_account_modal_spec.js @@ -0,0 +1,129 @@ +import Vue from 'vue'; + +import deleteAccountModal from '~/profile/account/components/delete_account_modal.vue'; + +import mountComponent from '../../../helpers/vue_mount_component_helper'; + +describe('DeleteAccountModal component', () => { + const actionUrl = `${gl.TEST_HOST}/delete/user`; + const username = 'hasnoname'; + let Component; + let vm; + + beforeEach(() => { + Component = Vue.extend(deleteAccountModal); + }); + + afterEach(() => { + vm.$destroy(); + }); + + const findElements = () => { + const confirmation = vm.confirmWithPassword ? 'password' : 'username'; + return { + form: vm.$refs.form, + input: vm.$el.querySelector(`[name="${confirmation}"]`), + submitButton: vm.$el.querySelector('.btn-danger'), + }; + }; + + describe('with password confirmation', () => { + beforeEach((done) => { + vm = mountComponent(Component, { + actionUrl, + confirmWithPassword: true, + username, + }); + + vm.isOpen = true; + + Vue.nextTick() + .then(done) + .catch(done.fail); + }); + + it('does not accept empty password', (done) => { + const { form, input, submitButton } = findElements(); + spyOn(form, 'submit'); + input.value = ''; + input.dispatchEvent(new Event('input')); + + Vue.nextTick() + .then(() => { + expect(vm.enteredPassword).toBe(input.value); + expect(submitButton).toHaveClass('disabled'); + submitButton.click(); + expect(form.submit).not.toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + it('submits form with password', (done) => { + const { form, input, submitButton } = findElements(); + spyOn(form, 'submit'); + input.value = 'anything'; + input.dispatchEvent(new Event('input')); + + Vue.nextTick() + .then(() => { + expect(vm.enteredPassword).toBe(input.value); + expect(submitButton).not.toHaveClass('disabled'); + submitButton.click(); + expect(form.submit).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('with username confirmation', () => { + beforeEach((done) => { + vm = mountComponent(Component, { + actionUrl, + confirmWithPassword: false, + username, + }); + + vm.isOpen = true; + + Vue.nextTick() + .then(done) + .catch(done.fail); + }); + + it('does not accept wrong username', (done) => { + const { form, input, submitButton } = findElements(); + spyOn(form, 'submit'); + input.value = 'this is wrong'; + input.dispatchEvent(new Event('input')); + + Vue.nextTick() + .then(() => { + expect(vm.enteredUsername).toBe(input.value); + expect(submitButton).toHaveClass('disabled'); + submitButton.click(); + expect(form.submit).not.toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + it('submits form with correct username', (done) => { + const { form, input, submitButton } = findElements(); + spyOn(form, 'submit'); + input.value = username; + input.dispatchEvent(new Event('input')); + + Vue.nextTick() + .then(() => { + expect(vm.enteredUsername).toBe(input.value); + expect(submitButton).not.toHaveClass('disabled'); + submitButton.click(); + expect(form.submit).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/lib/banzai/renderer_spec.rb b/spec/lib/banzai/renderer_spec.rb index da42272bbef..81a04a2d46d 100644 --- a/spec/lib/banzai/renderer_spec.rb +++ b/spec/lib/banzai/renderer_spec.rb @@ -31,7 +31,14 @@ describe Banzai::Renderer do let(:object) { fake_object(fresh: false) } it 'caches and returns the result' do - expect(object).to receive(:refresh_markdown_cache!).with(do_update: true) + expect(object).to receive(:refresh_markdown_cache!) + + is_expected.to eq('field_html') + end + + it "skips database caching on a GitLab read-only instance" do + allow(Gitlab::Database).to receive(:read_only?).and_return(true) + expect(object).to receive(:refresh_markdown_cache!) is_expected.to eq('field_html') end diff --git a/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb new file mode 100644 index 00000000000..1a4ea2bac48 --- /dev/null +++ b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb @@ -0,0 +1,117 @@ +require 'spec_helper' + +describe Gitlab::BackgroundMigration::CreateForkNetworkMembershipsRange, :migration, schema: 20170929131201 do + let(:migration) { described_class.new } + + let(:base1) { create(:project) } + let(:base1_fork1) { create(:project) } + let(:base1_fork2) { create(:project) } + + let(:base2) { create(:project) } + let(:base2_fork1) { create(:project) } + let(:base2_fork2) { create(:project) } + + let(:fork_of_fork) { create(:project) } + let(:fork_of_fork2) { create(:project) } + let(:second_level_fork) { create(:project) } + let(:third_level_fork) { create(:project) } + + let(:fork_network1) { fork_networks.find_by(root_project_id: base1.id) } + let(:fork_network2) { fork_networks.find_by(root_project_id: base2.id) } + + let!(:forked_project_links) { table(:forked_project_links) } + let!(:fork_networks) { table(:fork_networks) } + let!(:fork_network_members) { table(:fork_network_members) } + + before do + # The fork-network relation created for the forked project + fork_networks.create(id: 1, root_project_id: base1.id) + fork_network_members.create(project_id: base1.id, fork_network_id: 1) + fork_networks.create(id: 2, root_project_id: base2.id) + fork_network_members.create(project_id: base2.id, fork_network_id: 2) + + # Normal fork links + forked_project_links.create(id: 1, forked_from_project_id: base1.id, forked_to_project_id: base1_fork1.id) + forked_project_links.create(id: 2, forked_from_project_id: base1.id, forked_to_project_id: base1_fork2.id) + forked_project_links.create(id: 3, forked_from_project_id: base2.id, forked_to_project_id: base2_fork1.id) + forked_project_links.create(id: 4, forked_from_project_id: base2.id, forked_to_project_id: base2_fork2.id) + + # Fork links + forked_project_links.create(id: 5, forked_from_project_id: base1_fork1.id, forked_to_project_id: fork_of_fork.id) + forked_project_links.create(id: 6, forked_from_project_id: base1_fork1.id, forked_to_project_id: fork_of_fork2.id) + + # Forks 3 levels down + forked_project_links.create(id: 7, forked_from_project_id: fork_of_fork.id, forked_to_project_id: second_level_fork.id) + forked_project_links.create(id: 8, forked_from_project_id: second_level_fork.id, forked_to_project_id: third_level_fork.id) + + migration.perform(1, 8) + end + + it 'creates a memberships for the direct forks' do + base1_fork1_membership = fork_network_members.find_by(fork_network_id: fork_network1.id, + project_id: base1_fork1.id) + base1_fork2_membership = fork_network_members.find_by(fork_network_id: fork_network1.id, + project_id: base1_fork2.id) + base2_fork1_membership = fork_network_members.find_by(fork_network_id: fork_network2.id, + project_id: base2_fork1.id) + base2_fork2_membership = fork_network_members.find_by(fork_network_id: fork_network2.id, + project_id: base2_fork2.id) + + expect(base1_fork1_membership.forked_from_project_id).to eq(base1.id) + expect(base1_fork2_membership.forked_from_project_id).to eq(base1.id) + expect(base2_fork1_membership.forked_from_project_id).to eq(base2.id) + expect(base2_fork2_membership.forked_from_project_id).to eq(base2.id) + end + + it 'adds the fork network members for forks of forks' do + fork_of_fork_membership = fork_network_members.find_by(project_id: fork_of_fork.id, + fork_network_id: fork_network1.id) + fork_of_fork2_membership = fork_network_members.find_by(project_id: fork_of_fork2.id, + fork_network_id: fork_network1.id) + second_level_fork_membership = fork_network_members.find_by(project_id: second_level_fork.id, + fork_network_id: fork_network1.id) + third_level_fork_membership = fork_network_members.find_by(project_id: third_level_fork.id, + fork_network_id: fork_network1.id) + + expect(fork_of_fork_membership.forked_from_project_id).to eq(base1_fork1.id) + expect(fork_of_fork2_membership.forked_from_project_id).to eq(base1_fork1.id) + expect(second_level_fork_membership.forked_from_project_id).to eq(fork_of_fork.id) + expect(third_level_fork_membership.forked_from_project_id).to eq(second_level_fork.id) + end + + it 'reschedules itself when there are missing members' do + allow(migration).to receive(:missing_members?).and_return(true) + + expect(BackgroundMigrationWorker) + .to receive(:perform_in).with(described_class::RESCHEDULE_DELAY, "CreateForkNetworkMembershipsRange", [1, 3]) + + migration.perform(1, 3) + end + + it 'can be repeated without effect' do + expect { fork_network_members.count }.not_to change { migration.perform(1, 7) } + end + + it 'knows it is finished for this range' do + expect(migration.missing_members?(1, 7)).to be_falsy + end + + context 'with more forks' do + before do + forked_project_links.create(id: 9, forked_from_project_id: fork_of_fork.id, forked_to_project_id: create(:project).id) + forked_project_links.create(id: 10, forked_from_project_id: fork_of_fork.id, forked_to_project_id: create(:project).id) + end + + it 'only processes a single batch of links at a time' do + expect(fork_network_members.count).to eq(10) + + migration.perform(8, 10) + + expect(fork_network_members.count).to eq(12) + end + + it 'knows when not all memberships withing a batch have been created' do + expect(migration.missing_members?(8, 10)).to be_truthy + end + end +end diff --git a/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb new file mode 100644 index 00000000000..3ef1873e615 --- /dev/null +++ b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb @@ -0,0 +1,85 @@ +require 'spec_helper' + +describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, schema: 20170929131201 do + let(:migration) { described_class.new } + let(:base1) { create(:project) } + let(:base1_fork1) { create(:project) } + let(:base1_fork2) { create(:project) } + + let(:base2) { create(:project) } + let(:base2_fork1) { create(:project) } + let(:base2_fork2) { create(:project) } + + let!(:forked_project_links) { table(:forked_project_links) } + let!(:fork_networks) { table(:fork_networks) } + let!(:fork_network_members) { table(:fork_network_members) } + + let(:fork_network1) { fork_networks.find_by(root_project_id: base1.id) } + let(:fork_network2) { fork_networks.find_by(root_project_id: base2.id) } + + before do + # A normal fork link + forked_project_links.create(id: 1, + forked_from_project_id: base1.id, + forked_to_project_id: base1_fork1.id) + forked_project_links.create(id: 2, + forked_from_project_id: base1.id, + forked_to_project_id: base1_fork2.id) + + forked_project_links.create(id: 3, + forked_from_project_id: base2.id, + forked_to_project_id: base2_fork1.id) + forked_project_links.create(id: 4, + forked_from_project_id: base2_fork1.id, + forked_to_project_id: create(:project).id) + + forked_project_links.create(id: 5, + forked_from_project_id: base2.id, + forked_to_project_id: base2_fork2.id) + + migration.perform(1, 3) + end + + it 'it creates the fork network' do + expect(fork_network1).not_to be_nil + expect(fork_network2).not_to be_nil + end + + it 'does not create a fork network for a fork-of-fork' do + # perfrom the entire batch + migration.perform(1, 5) + + expect(fork_networks.find_by(root_project_id: base2_fork1.id)).to be_nil + end + + it 'creates memberships for the root of fork networks' do + base1_membership = fork_network_members.find_by(fork_network_id: fork_network1.id, + project_id: base1.id) + base2_membership = fork_network_members.find_by(fork_network_id: fork_network2.id, + project_id: base2.id) + + expect(base1_membership).not_to be_nil + expect(base2_membership).not_to be_nil + end + + it 'schedules a job for inserting memberships for forks-of-forks' do + delay = Gitlab::BackgroundMigration::CreateForkNetworkMembershipsRange::RESCHEDULE_DELAY + + expect(BackgroundMigrationWorker) + .to receive(:perform_in).with(delay, "CreateForkNetworkMembershipsRange", [1, 3]) + + migration.perform(1, 3) + end + + it 'only processes a single batch of links at a time' do + expect(fork_network_members.count).to eq(5) + + migration.perform(3, 5) + + expect(fork_network_members.count).to eq(7) + end + + it 'can be repeated without effect' do + expect { migration.perform(1, 3) }.not_to change { fork_network_members.count } + end +end diff --git a/spec/lib/gitlab/ci/trace/section_parser_spec.rb b/spec/lib/gitlab/ci/trace/section_parser_spec.rb new file mode 100644 index 00000000000..ca53ff87c6f --- /dev/null +++ b/spec/lib/gitlab/ci/trace/section_parser_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper' + +describe Gitlab::Ci::Trace::SectionParser do + def lines_with_pos(text) + pos = 0 + StringIO.new(text).each_line do |line| + yield line, pos + pos += line.bytesize + 1 # newline + end + end + + def build_lines(text) + to_enum(:lines_with_pos, text) + end + + def section(name, start, duration, text) + end_ = start + duration + "section_start:#{start.to_i}:#{name}\r\033[0K#{text}section_end:#{end_.to_i}:#{name}\r\033[0K" + end + + let(:lines) { build_lines('') } + subject { described_class.new(lines) } + + describe '#sections' do + before do + subject.parse! + end + + context 'empty trace' do + let(:lines) { build_lines('') } + + it { expect(subject.sections).to be_empty } + end + + context 'with a sectionless trace' do + let(:lines) { build_lines("line 1\nline 2\n") } + + it { expect(subject.sections).to be_empty } + end + + context 'with trace markers' do + let(:start_time) { Time.new(2017, 10, 5).utc } + let(:section_b_duration) { 1.second } + let(:section_a) { section('a', start_time, 0, 'a line') } + let(:section_b) { section('b', start_time, section_b_duration, "another line\n") } + let(:lines) { build_lines(section_a + section_b) } + + it { expect(subject.sections.size).to eq(2) } + it { expect(subject.sections[1][:name]).to eq('b') } + it { expect(subject.sections[1][:date_start]).to eq(start_time) } + it { expect(subject.sections[1][:date_end]).to eq(start_time + section_b_duration) } + end + end + + describe '#parse!' do + context 'multiple "section_" but no complete markers' do + let(:lines) { build_lines('section_section_section_') } + + it 'must find 3 possible section start but no complete sections' do + expect(subject).to receive(:find_next_marker).exactly(3).times.and_call_original + + subject.parse! + + expect(subject.sections).to be_empty + end + end + + context 'trace with UTF-8 chars' do + let(:line) { 'GitLab ❤️ 狸 (tanukis)\n' } + let(:trace) { section('test_section', Time.new(2017, 10, 5).utc, 3.seconds, line) } + let(:lines) { build_lines(trace) } + + it 'must handle correctly byte positioning' do + expect(subject).to receive(:find_next_marker).exactly(2).times.and_call_original + + subject.parse! + + sections = subject.sections + + expect(sections.size).to eq(1) + s = sections[0] + len = s[:byte_end] - s[:byte_start] + expect(trace.byteslice(s[:byte_start], len)).to eq(line) + end + end + end +end diff --git a/spec/lib/gitlab/ci/trace_spec.rb b/spec/lib/gitlab/ci/trace_spec.rb index 9cb0b62590a..3546532b9b4 100644 --- a/spec/lib/gitlab/ci/trace_spec.rb +++ b/spec/lib/gitlab/ci/trace_spec.rb @@ -61,6 +61,93 @@ describe Gitlab::Ci::Trace do end end + describe '#extract_sections' do + let(:log) { 'No sections' } + let(:sections) { trace.extract_sections } + + before do + trace.set(log) + end + + context 'no sections' do + it 'returs []' do + expect(trace.extract_sections).to eq([]) + end + end + + context 'multiple sections available' do + let(:log) { File.read(expand_fixture_path('trace/trace_with_sections')) } + let(:sections_data) do + [ + { name: 'prepare_script', lines: 2, duration: 3.seconds }, + { name: 'get_sources', lines: 4, duration: 1.second }, + { name: 'restore_cache', lines: 0, duration: 0.seconds }, + { name: 'download_artifacts', lines: 0, duration: 0.seconds }, + { name: 'build_script', lines: 2, duration: 1.second }, + { name: 'after_script', lines: 0, duration: 0.seconds }, + { name: 'archive_cache', lines: 0, duration: 0.seconds }, + { name: 'upload_artifacts', lines: 0, duration: 0.seconds } + ] + end + + it "returns valid sections" do + expect(sections).not_to be_empty + expect(sections.size).to eq(sections_data.size), + "expected #{sections_data.size} sections, got #{sections.size}" + + buff = StringIO.new(log) + sections.each_with_index do |s, i| + expected = sections_data[i] + + expect(s[:name]).to eq(expected[:name]) + expect(s[:date_end] - s[:date_start]).to eq(expected[:duration]) + + buff.seek(s[:byte_start], IO::SEEK_SET) + length = s[:byte_end] - s[:byte_start] + lines = buff.read(length).count("\n") + expect(lines).to eq(expected[:lines]) + end + end + end + + context 'logs contains "section_start"' do + let(:log) { "section_start:1506417476:a_section\r\033[0Klooks like a section_start:invalid\nsection_end:1506417477:a_section\r\033[0K"} + + it "returns only one section" do + expect(sections).not_to be_empty + expect(sections.size).to eq(1) + + section = sections[0] + expect(section[:name]).to eq('a_section') + expect(section[:byte_start]).not_to eq(section[:byte_end]), "got an empty section" + end + end + + context 'missing section_end' do + let(:log) { "section_start:1506417476:a_section\r\033[0KSome logs\nNo section_end\n"} + + it "returns no sections" do + expect(sections).to be_empty + end + end + + context 'missing section_start' do + let(:log) { "Some logs\nNo section_start\nsection_end:1506417476:a_section\r\033[0K"} + + it "returns no sections" do + expect(sections).to be_empty + end + end + + context 'inverted section_start section_end' do + let(:log) { "section_end:1506417476:a_section\r\033[0Klooks like a section_start:invalid\nsection_start:1506417477:a_section\r\033[0K"} + + it "returns no sections" do + expect(sections).to be_empty + end + end + end + describe '#set' do before do trace.set("12") diff --git a/spec/lib/gitlab/diff/formatters/image_formatter_spec.rb b/spec/lib/gitlab/diff/formatters/image_formatter_spec.rb new file mode 100644 index 00000000000..2f99febe04e --- /dev/null +++ b/spec/lib/gitlab/diff/formatters/image_formatter_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe Gitlab::Diff::Formatters::ImageFormatter do + it_behaves_like "position formatter" do + let(:base_attrs) do + { + base_sha: 123, + start_sha: 456, + head_sha: 789, + old_path: 'old_image.png', + new_path: 'new_image.png', + position_type: 'image' + } + end + + let(:attrs) do + base_attrs.merge(width: 100, height: 100, x: 1, y: 2) + end + end +end diff --git a/spec/lib/gitlab/diff/formatters/text_formatter_spec.rb b/spec/lib/gitlab/diff/formatters/text_formatter_spec.rb new file mode 100644 index 00000000000..897dc917f6a --- /dev/null +++ b/spec/lib/gitlab/diff/formatters/text_formatter_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +describe Gitlab::Diff::Formatters::TextFormatter do + let!(:base) do + { + base_sha: 123, + start_sha: 456, + head_sha: 789, + old_path: 'old_path.txt', + new_path: 'new_path.txt' + } + end + + let!(:complete) do + base.merge(old_line: 1, new_line: 2) + end + + it_behaves_like "position formatter" do + let(:base_attrs) { base } + + let(:attrs) { complete } + end + + # Specific text formatter examples + let!(:formatter) { described_class.new(attrs) } + + describe '#line_age' do + subject { formatter.line_age } + + context ' when there is only new_line' do + let(:attrs) { base.merge(new_line: 1) } + + it { is_expected.to eq('new') } + end + + context ' when there is only old_line' do + let(:attrs) { base.merge(old_line: 1) } + + it { is_expected.to eq('old') } + end + end +end diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb index 7798736a4dc..9bf54fdecc4 100644 --- a/spec/lib/gitlab/diff/position_spec.rb +++ b/spec/lib/gitlab/diff/position_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::Diff::Position do let(:project) { create(:project, :repository) } - describe "position for an added file" do + describe "position for an added text file" do let(:commit) { project.commit("2ea1f3dec713d940208fb5ce4a38765ecb5d3f73") } subject do @@ -47,6 +47,31 @@ describe Gitlab::Diff::Position do end end + describe "position for an added image file" do + let(:commit) { project.commit("33f3729a45c02fc67d00adb1b8bca394b0e761d9") } + + subject do + described_class.new( + old_path: "files/images/6049019_460s.jpg", + new_path: "files/images/6049019_460s.jpg", + width: 100, + height: 100, + x: 1, + y: 100, + diff_refs: commit.diff_refs, + position_type: "image" + ) + end + + it "returns the correct diff file" do + diff_file = subject.diff_file(project.repository) + + expect(diff_file.new_file?).to be true + expect(diff_file.new_path).to eq(subject.new_path) + expect(diff_file.diff_refs).to eq(subject.diff_refs) + end + end + describe "position for a changed file" do let(:commit) { project.commit("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") } @@ -468,26 +493,54 @@ describe Gitlab::Diff::Position do end describe "#to_json" do - let(:hash) do - { - old_path: "files/ruby/popen.rb", - new_path: "files/ruby/popen.rb", - old_line: nil, - new_line: 14, - base_sha: nil, - head_sha: nil, - start_sha: nil - } + shared_examples "diff position json" do + it "returns the position as JSON" do + expect(JSON.parse(diff_position.to_json)).to eq(hash.stringify_keys) + end + + it "works when nested under another hash" do + expect(JSON.parse(JSON.generate(pos: diff_position))).to eq('pos' => hash.stringify_keys) + end end - let(:diff_position) { described_class.new(hash) } + context "for text positon" do + let(:hash) do + { + old_path: "files/ruby/popen.rb", + new_path: "files/ruby/popen.rb", + old_line: nil, + new_line: 14, + base_sha: nil, + head_sha: nil, + start_sha: nil, + position_type: "text" + } + end + + let(:diff_position) { described_class.new(hash) } - it "returns the position as JSON" do - expect(JSON.parse(diff_position.to_json)).to eq(hash.stringify_keys) + it_behaves_like "diff position json" end - it "works when nested under another hash" do - expect(JSON.parse(JSON.generate(pos: diff_position))).to eq('pos' => hash.stringify_keys) + context "for image positon" do + let(:hash) do + { + old_path: "files/any.img", + new_path: "files/any.img", + base_sha: nil, + head_sha: nil, + start_sha: nil, + width: 100, + height: 100, + x: 1, + y: 100, + position_type: "image" + } + end + + let(:diff_position) { described_class.new(hash) } + + it_behaves_like "diff position json" end end end diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb index 4fa30d8df8b..e5138705443 100644 --- a/spec/lib/gitlab/diff/position_tracer_spec.rb +++ b/spec/lib/gitlab/diff/position_tracer_spec.rb @@ -71,6 +71,10 @@ describe Gitlab::Diff::PositionTracer do Gitlab::Diff::DiffRefs.new(base_sha: base_commit.id, head_sha: head_commit.id) end + def text_position_attrs + [:old_line, :new_line] + end + def position(attrs = {}) attrs.reverse_merge!( diff_refs: old_diff_refs @@ -91,7 +95,11 @@ describe Gitlab::Diff::PositionTracer do expect(new_position.diff_refs).to eq(new_diff_refs) attrs.each do |attr, value| - expect(new_position.send(attr)).to eq(value) + if text_position_attrs.include?(attr) + expect(new_position.formatter.send(attr)).to eq(value) + else + expect(new_position.send(attr)).to eq(value) + end end end end @@ -110,7 +118,11 @@ describe Gitlab::Diff::PositionTracer do expect(change_position.diff_refs).to eq(change_diff_refs) attrs.each do |attr, value| - expect(change_position.send(attr)).to eq(value) + if text_position_attrs.include?(attr) + expect(change_position.formatter.send(attr)).to eq(value) + else + expect(change_position.send(attr)).to eq(value) + end end end end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 458627ee4de..c54327bd2e4 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -598,6 +598,19 @@ describe Gitlab::GitAccess do admin: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false })) end end + + context "when in a read-only GitLab instance" do + before do + create(:protected_branch, name: 'feature', project: project) + allow(Gitlab::Database).to receive(:read_only?) { true } + end + + # Only check admin; if an admin can't do it, other roles can't either + matrix = permissions_matrix[:admin].dup + matrix.each { |key, _| matrix[key] = false } + + run_permission_checks(admin: matrix) + end end describe 'build authentication abilities' do @@ -632,6 +645,16 @@ describe Gitlab::GitAccess do end end + context 'when the repository is read only' do + let(:project) { create(:project, :repository, :read_only) } + + it 'denies push access' do + project.add_master(user) + + expect { push_access_check }.to raise_unauthorized('The repository is temporarily read-only. Please try again later.') + end + end + describe 'deploy key permissions' do let(:key) { create(:deploy_key, user: user, can_push: can_push) } let(:actor) { key } diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb index 0376b4ee783..1056074264a 100644 --- a/spec/lib/gitlab/git_access_wiki_spec.rb +++ b/spec/lib/gitlab/git_access_wiki_spec.rb @@ -4,6 +4,7 @@ describe Gitlab::GitAccessWiki do let(:access) { described_class.new(user, project, 'web', authentication_abilities: authentication_abilities, redirected_path: redirected_path) } let(:project) { create(:project, :repository) } let(:user) { create(:user) } + let(:changes) { ['6f6d7e7ed 570e7b2ab refs/heads/master'] } let(:redirected_path) { nil } let(:authentication_abilities) do [ @@ -13,19 +14,27 @@ describe Gitlab::GitAccessWiki do ] end - describe 'push_allowed?' do - before do - create(:protected_branch, name: 'master', project: project) - project.team << [user, :developer] - end + describe '#push_access_check' do + context 'when user can :create_wiki' do + before do + create(:protected_branch, name: 'master', project: project) + project.team << [user, :developer] + end - subject { access.check('git-receive-pack', changes) } + subject { access.check('git-receive-pack', changes) } - it { expect { subject }.not_to raise_error } - end + it { expect { subject }.not_to raise_error } + + context 'when in a read-only GitLab instance' do + before do + allow(Gitlab::Database).to receive(:read_only?) { true } + end - def changes - ['6f6d7e7ed 570e7b2ab refs/heads/master'] + it 'does not give access to upload wiki code' do + expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "You can't push code to a read-only GitLab instance.") + end + end + end end describe '#access_check_download!' do diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index ec425fd2803..29baa70d5ae 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -271,6 +271,10 @@ project: - container_repositories - uploads - members_and_requesters +- build_trace_section_names +- root_of_fork_network +- fork_network_member +- fork_network award_emoji: - awardable - user diff --git a/spec/lib/gitlab/import_export/fork_spec.rb b/spec/lib/gitlab/import_export/fork_spec.rb index c7fbc2bc92f..dd0ce0dae41 100644 --- a/spec/lib/gitlab/import_export/fork_spec.rb +++ b/spec/lib/gitlab/import_export/fork_spec.rb @@ -1,13 +1,15 @@ require 'spec_helper' describe 'forked project import' do + include ProjectForksHelper + let(:user) { create(:user) } let!(:project_with_repo) { create(:project, :repository, name: 'test-repo-restorer', path: 'test-repo-restorer') } let!(:project) { create(:project, name: 'test-repo-restorer-no-repo', path: 'test-repo-restorer-no-repo') } let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.full_path) } let(:forked_from_project) { create(:project, :repository) } - let(:fork_link) { create(:forked_project_link, forked_from_project: project_with_repo) } + let(:forked_project) { fork_project(project_with_repo, nil, repository: true) } let(:repo_saver) { Gitlab::ImportExport::RepoSaver.new(project: project_with_repo, shared: shared) } let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) } @@ -16,7 +18,7 @@ describe 'forked project import' do end let!(:merge_request) do - create(:merge_request, source_project: fork_link.forked_to_project, target_project: project_with_repo) + create(:merge_request, source_project: forked_project, target_project: project_with_repo) end let(:saver) do diff --git a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb index 4d87f27ce05..473ba40fae7 100644 --- a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb +++ b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb @@ -1,13 +1,14 @@ require 'spec_helper' describe Gitlab::ImportExport::MergeRequestParser do + include ProjectForksHelper + let(:user) { create(:user) } let!(:project) { create(:project, :repository, name: 'test-repo-restorer', path: 'test-repo-restorer') } - let(:forked_from_project) { create(:project, :repository) } - let(:fork_link) { create(:forked_project_link, forked_from_project: project) } + let(:forked_project) { fork_project(project) } let!(:merge_request) do - create(:merge_request, source_project: fork_link.forked_to_project, target_project: project) + create(:merge_request, source_project: forked_project, target_project: project) end let(:parsed_merge_request) do diff --git a/spec/lib/gitlab/middleware/read_only_spec.rb b/spec/lib/gitlab/middleware/read_only_spec.rb new file mode 100644 index 00000000000..742a792a1af --- /dev/null +++ b/spec/lib/gitlab/middleware/read_only_spec.rb @@ -0,0 +1,142 @@ +require 'spec_helper' + +describe Gitlab::Middleware::ReadOnly do + include Rack::Test::Methods + + RSpec::Matchers.define :be_a_redirect do + match do |response| + response.status == 301 + end + end + + RSpec::Matchers.define :disallow_request do + match do |middleware| + flash = middleware.send(:rack_flash) + flash['alert'] && flash['alert'].include?('You cannot do writing operations') + end + end + + RSpec::Matchers.define :disallow_request_in_json do + match do |response| + json_response = JSON.parse(response.body) + response.body.include?('You cannot do writing operations') && json_response.key?('message') + end + end + + let(:rack_stack) do + rack = Rack::Builder.new do + use ActionDispatch::Session::CacheStore + use ActionDispatch::Flash + use ActionDispatch::ParamsParser + end + + rack.run(subject) + rack.to_app + end + + subject { described_class.new(fake_app) } + + let(:request) { Rack::MockRequest.new(rack_stack) } + + context 'normal requests to a read-only Gitlab instance' do + let(:fake_app) { lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ['OK']] } } + + before do + allow(Gitlab::Database).to receive(:read_only?) { true } + end + + it 'expects PATCH requests to be disallowed' do + response = request.patch('/test_request') + + expect(response).to be_a_redirect + expect(subject).to disallow_request + end + + it 'expects PUT requests to be disallowed' do + response = request.put('/test_request') + + expect(response).to be_a_redirect + expect(subject).to disallow_request + end + + it 'expects POST requests to be disallowed' do + response = request.post('/test_request') + + expect(response).to be_a_redirect + expect(subject).to disallow_request + end + + it 'expects a internal POST request to be allowed after a disallowed request' do + response = request.post('/test_request') + + expect(response).to be_a_redirect + + response = request.post("/api/#{API::API.version}/internal") + + expect(response).not_to be_a_redirect + end + + it 'expects DELETE requests to be disallowed' do + response = request.delete('/test_request') + + expect(response).to be_a_redirect + expect(subject).to disallow_request + end + + context 'whitelisted requests' do + it 'expects DELETE request to logout to be allowed' do + response = request.delete('/users/sign_out') + + expect(response).not_to be_a_redirect + expect(subject).not_to disallow_request + end + + it 'expects a POST internal request to be allowed' do + response = request.post("/api/#{API::API.version}/internal") + + expect(response).not_to be_a_redirect + expect(subject).not_to disallow_request + end + + it 'expects a POST LFS request to batch URL to be allowed' do + response = request.post('/root/rouge.git/info/lfs/objects/batch') + + expect(response).not_to be_a_redirect + expect(subject).not_to disallow_request + end + end + end + + context 'json requests to a read-only GitLab instance' do + let(:fake_app) { lambda { |env| [200, { 'Content-Type' => 'application/json' }, ['OK']] } } + let(:content_json) { { 'CONTENT_TYPE' => 'application/json' } } + + before do + allow(Gitlab::Database).to receive(:read_only?) { true } + end + + it 'expects PATCH requests to be disallowed' do + response = request.patch('/test_request', content_json) + + expect(response).to disallow_request_in_json + end + + it 'expects PUT requests to be disallowed' do + response = request.put('/test_request', content_json) + + expect(response).to disallow_request_in_json + end + + it 'expects POST requests to be disallowed' do + response = request.post('/test_request', content_json) + + expect(response).to disallow_request_in_json + end + + it 'expects DELETE requests to be disallowed' do + response = request.delete('/test_request', content_json) + + expect(response).to disallow_request_in_json + end + end +end diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb index 4c5efbde69a..e44a7c23452 100644 --- a/spec/lib/gitlab/search_results_spec.rb +++ b/spec/lib/gitlab/search_results_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Gitlab::SearchResults do + include ProjectForksHelper + let(:user) { create(:user) } let!(:project) { create(:project, name: 'foo') } let!(:issue) { create(:issue, project: project, title: 'foo') } @@ -42,7 +44,7 @@ describe Gitlab::SearchResults do end it 'includes merge requests from source and target projects' do - forked_project = create(:project, forked_from_project: project) + forked_project = fork_project(project, user) merge_request_2 = create(:merge_request, target_project: project, source_project: forked_project, title: 'foo') results = described_class.new(user, Project.where(id: forked_project.id), 'foo') diff --git a/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb b/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb index a0fb86345f3..b4b83b70d1c 100644 --- a/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb +++ b/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb @@ -39,6 +39,14 @@ describe SystemCheck::App::GitUserDefaultSSHConfigCheck do it { is_expected.to eq(expected_result) } end + + it 'skips GitLab read-only instances' do + stub_user + stub_home_dir + allow(Gitlab::Database).to receive(:read_only?).and_return(true) + + is_expected.to be_truthy + end end describe '#check?' do diff --git a/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb b/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb index 862907c5d01..84c2e9f7e52 100644 --- a/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb +++ b/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb @@ -2,11 +2,12 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170508170547_add_head_pipeline_for_each_merge_request.rb') describe AddHeadPipelineForEachMergeRequest, :truncate do + include ProjectForksHelper + let(:migration) { described_class.new } let!(:project) { create(:project) } - let!(:forked_project_link) { create(:forked_project_link, forked_from_project: project) } - let!(:other_project) { forked_project_link.forked_to_project } + let!(:other_project) { fork_project(project) } let!(:pipeline_1) { create(:ci_pipeline, project: project, ref: "branch_1") } let!(:pipeline_2) { create(:ci_pipeline, project: other_project, ref: "branch_1") } diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 451968c7342..06f76b5501e 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -18,6 +18,7 @@ describe Ci::Build do it { is_expected.to belong_to(:trigger_request) } it { is_expected.to belong_to(:erased_by) } it { is_expected.to have_many(:deployments) } + it { is_expected.to have_many(:trace_sections)} it { is_expected.to validate_presence_of(:ref) } it { is_expected.to respond_to(:has_trace?) } it { is_expected.to respond_to(:trace) } @@ -320,6 +321,17 @@ describe Ci::Build do end end + describe '#parse_trace_sections!' do + it 'calls ExtractSectionsFromBuildTraceService' do + expect(Ci::ExtractSectionsFromBuildTraceService) + .to receive(:new).with(project, build.user).once.and_call_original + expect_any_instance_of(Ci::ExtractSectionsFromBuildTraceService) + .to receive(:execute).with(build).once + + build.parse_trace_sections! + end + end + describe '#trace' do subject { build.trace } diff --git a/spec/models/ci/build_trace_section_name_spec.rb b/spec/models/ci/build_trace_section_name_spec.rb new file mode 100644 index 00000000000..386ee6880cb --- /dev/null +++ b/spec/models/ci/build_trace_section_name_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe Ci::BuildTraceSectionName, model: true do + subject { build(:ci_build_trace_section_name) } + + it { is_expected.to belong_to(:project) } + it { is_expected.to have_many(:trace_sections)} + + it { is_expected.to validate_presence_of(:project) } + it { is_expected.to validate_presence_of(:name) } + it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) } +end diff --git a/spec/models/ci/build_trace_section_spec.rb b/spec/models/ci/build_trace_section_spec.rb new file mode 100644 index 00000000000..541a9a36fb8 --- /dev/null +++ b/spec/models/ci/build_trace_section_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +describe Ci::BuildTraceSection, model: true do + it { is_expected.to belong_to(:build)} + it { is_expected.to belong_to(:project)} + it { is_expected.to belong_to(:section_name)} + + it { is_expected.to validate_presence_of(:section_name) } + it { is_expected.to validate_presence_of(:build) } + it { is_expected.to validate_presence_of(:project) } +end diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb index 40bbb10eaac..129dfa07f15 100644 --- a/spec/models/concerns/cache_markdown_field_spec.rb +++ b/spec/models/concerns/cache_markdown_field_spec.rb @@ -178,57 +178,59 @@ describe CacheMarkdownField do end end - describe '#refresh_markdown_cache!' do + describe '#refresh_markdown_cache' do before do thing.foo = updated_markdown end - context 'do_update: false' do - it 'fills all html fields' do - thing.refresh_markdown_cache! + it 'fills all html fields' do + thing.refresh_markdown_cache - expect(thing.foo_html).to eq(updated_html) - expect(thing.foo_html_changed?).to be_truthy - expect(thing.baz_html_changed?).to be_truthy - end + expect(thing.foo_html).to eq(updated_html) + expect(thing.foo_html_changed?).to be_truthy + expect(thing.baz_html_changed?).to be_truthy + end - it 'does not save the result' do - expect(thing).not_to receive(:update_columns) + it 'does not save the result' do + expect(thing).not_to receive(:update_columns) - thing.refresh_markdown_cache! - end + thing.refresh_markdown_cache + end - it 'updates the markdown cache version' do - thing.cached_markdown_version = nil - thing.refresh_markdown_cache! + it 'updates the markdown cache version' do + thing.cached_markdown_version = nil + thing.refresh_markdown_cache - expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) - end + expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) end + end - context 'do_update: true' do - it 'fills all html fields' do - thing.refresh_markdown_cache!(do_update: true) + describe '#refresh_markdown_cache!' do + before do + thing.foo = updated_markdown + end - expect(thing.foo_html).to eq(updated_html) - expect(thing.foo_html_changed?).to be_truthy - expect(thing.baz_html_changed?).to be_truthy - end + it 'fills all html fields' do + thing.refresh_markdown_cache! - it 'skips saving if not persisted' do - expect(thing).to receive(:persisted?).and_return(false) - expect(thing).not_to receive(:update_columns) + expect(thing.foo_html).to eq(updated_html) + expect(thing.foo_html_changed?).to be_truthy + expect(thing.baz_html_changed?).to be_truthy + end - thing.refresh_markdown_cache!(do_update: true) - end + it 'skips saving if not persisted' do + expect(thing).to receive(:persisted?).and_return(false) + expect(thing).not_to receive(:update_columns) - it 'saves the changes using #update_columns' do - expect(thing).to receive(:persisted?).and_return(true) - expect(thing).to receive(:update_columns) - .with("foo_html" => updated_html, "baz_html" => "", "cached_markdown_version" => CacheMarkdownField::CACHE_VERSION) + thing.refresh_markdown_cache! + end - thing.refresh_markdown_cache!(do_update: true) - end + it 'saves the changes using #update_columns' do + expect(thing).to receive(:persisted?).and_return(true) + expect(thing).to receive(:update_columns) + .with("foo_html" => updated_html, "baz_html" => "", "cached_markdown_version" => CacheMarkdownField::CACHE_VERSION) + + thing.refresh_markdown_cache! end end diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb index b463d12e448..ab8773b7ede 100644 --- a/spec/models/concerns/routable_spec.rb +++ b/spec/models/concerns/routable_spec.rb @@ -12,6 +12,16 @@ describe Group, 'Routable' do it { is_expected.to have_many(:redirect_routes).dependent(:destroy) } end + describe 'GitLab read-only instance' do + it 'does not save route if route is not present' do + group.route.path = '' + allow(Gitlab::Database).to receive(:read_only?).and_return(true) + expect(group).to receive(:update_route).and_call_original + + expect { group.full_path }.to change { Route.count }.by(0) + end + end + describe 'Callbacks' do it 'creates route record on create' do expect(group.route.path).to eq(group.path) diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb index 4aa9ec789a3..eb0a3e9e0d3 100644 --- a/spec/models/diff_note_spec.rb +++ b/spec/models/diff_note_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe DiffNote do include RepoHelpers - let(:merge_request) { create(:merge_request) } + let!(:merge_request) { create(:merge_request) } let(:project) { merge_request.project } let(:commit) { project.commit(sample_commit.id) } @@ -98,14 +98,14 @@ describe DiffNote do diff_line = subject.diff_line expect(diff_line.added?).to be true - expect(diff_line.new_line).to eq(position.new_line) + expect(diff_line.new_line).to eq(position.formatter.new_line) expect(diff_line.text).to eq("+ vars = {") end end describe "#line_code" do it "returns the correct line code" do - line_code = Gitlab::Diff::LineCode.generate(position.file_path, position.new_line, 15) + line_code = Gitlab::Diff::LineCode.generate(position.file_path, position.formatter.new_line, 15) expect(subject.line_code).to eq(line_code) end @@ -255,4 +255,38 @@ describe DiffNote do end end end + + describe "image diff notes" do + let(:path) { "files/images/any_image.png" } + + let!(:position) do + Gitlab::Diff::Position.new( + old_path: path, + new_path: path, + width: 10, + height: 10, + x: 1, + y: 1, + diff_refs: merge_request.diff_refs, + position_type: "image" + ) + end + + describe "validations" do + subject { build(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request) } + + it { is_expected.not_to validate_presence_of(:line_code) } + + it "does not validate diff line" do + diff_line = subject.diff_line + + expect(diff_line).to be nil + expect(subject).to be_valid + end + end + + it "returns true for on_image?" do + expect(subject.on_image?).to be_truthy + end + end end diff --git a/spec/models/fork_network_member_spec.rb b/spec/models/fork_network_member_spec.rb new file mode 100644 index 00000000000..532ca1fca8c --- /dev/null +++ b/spec/models/fork_network_member_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' + +describe ForkNetworkMember do + describe 'validations' do + it { is_expected.to validate_presence_of(:project) } + it { is_expected.to validate_presence_of(:fork_network) } + end +end diff --git a/spec/models/fork_network_spec.rb b/spec/models/fork_network_spec.rb new file mode 100644 index 00000000000..605ccd6db06 --- /dev/null +++ b/spec/models/fork_network_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe ForkNetwork do + include ProjectForksHelper + + describe '#add_root_as_member' do + it 'adds the root project as a member when creating a new root network' do + project = create(:project) + fork_network = described_class.create(root_project: project) + + expect(fork_network.projects).to include(project) + end + end + + describe '#find_fork_in' do + it 'finds all fork of the current network in al collection' do + network = create(:fork_network) + root_project = network.root_project + another_project = fork_project(root_project) + create(:project) + + expect(network.find_forks_in(Project.all)) + .to contain_exactly(another_project, root_project) + end + end + + context 'for a deleted project' do + it 'keeps the fork network' do + project = create(:project, :public) + forked = fork_project(project) + project.destroy! + + fork_network = forked.reload.fork_network + + expect(fork_network.projects).to contain_exactly(forked) + expect(fork_network.root_project).to be_nil + end + + it 'allows multiple fork networks where the root project is deleted' do + first_project = create(:project) + second_project = create(:project) + first_fork = fork_project(first_project) + second_fork = fork_project(second_project) + + first_project.destroy + second_project.destroy + + expect(first_fork.fork_network).not_to be_nil + expect(first_fork.fork_network.root_project).to be_nil + expect(second_fork.fork_network).not_to be_nil + expect(second_fork.fork_network.root_project).to be_nil + end + end +end diff --git a/spec/models/forked_project_link_spec.rb b/spec/models/forked_project_link_spec.rb index 7dbeb4d2e74..32e33e8f42f 100644 --- a/spec/models/forked_project_link_spec.rb +++ b/spec/models/forked_project_link_spec.rb @@ -1,10 +1,11 @@ require 'spec_helper' describe ForkedProjectLink, "add link on fork" do + include ProjectForksHelper + let(:project_from) { create(:project, :repository) } let(:project_to) { fork_project(project_from, user) } let(:user) { create(:user) } - let(:namespace) { user.namespace } before do project_from.add_reporter(user) @@ -64,13 +65,4 @@ describe ForkedProjectLink, "add link on fork" do expect(ForkedProjectLink.exists?(id: forked_project_link.id)).to eq(false) end end - - def fork_project(from_project, user) - service = Projects::ForkService.new(from_project, user) - shell = double('gitlab_shell', fork_repository: true) - - allow(service).to receive(:gitlab_shell).and_return(shell) - - service.execute - end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 188a0a98ec3..950af653c80 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe MergeRequest do include RepoHelpers + include ProjectForksHelper subject { create(:merge_request) } @@ -49,6 +50,19 @@ describe MergeRequest do expect(subject).to be_valid end end + + context 'for forks' do + let(:project) { create(:project) } + let(:fork1) { fork_project(project) } + let(:fork2) { fork_project(project) } + + it 'allows merge requests for sibling-forks' do + subject.source_project = fork1 + subject.target_project = fork2 + + expect(subject).to be_valid + end + end end describe 'respond to' do @@ -672,7 +686,7 @@ describe MergeRequest do describe '#diverged_commits_count' do let(:project) { create(:project, :repository) } - let(:fork_project) { create(:project, :repository, forked_from_project: project) } + let(:forked_project) { fork_project(project, nil, repository: true) } context 'when the target branch does not exist anymore' do subject { create(:merge_request, source_project: project, target_project: project) } @@ -700,7 +714,7 @@ describe MergeRequest do end context 'diverged on fork' do - subject(:merge_request_fork_with_divergence) { create(:merge_request, :diverged, source_project: fork_project, target_project: project) } + subject(:merge_request_fork_with_divergence) { create(:merge_request, :diverged, source_project: forked_project, target_project: project) } it 'counts commits that are on target branch but not on source branch' do expect(subject.diverged_commits_count).to eq(29) @@ -708,7 +722,7 @@ describe MergeRequest do end context 'rebased on fork' do - subject(:merge_request_rebased) { create(:merge_request, :rebased, source_project: fork_project, target_project: project) } + subject(:merge_request_rebased) { create(:merge_request, :rebased, source_project: forked_project, target_project: project) } it 'counts commits that are on target branch but not on source branch' do expect(subject.diverged_commits_count).to eq(0) @@ -1257,11 +1271,7 @@ describe MergeRequest do end context 'with environments on source project' do - let(:source_project) do - create(:project, :repository) do |fork_project| - fork_project.create_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id) - end - end + let(:source_project) { fork_project(project, nil, repository: true) } let(:merge_request) do create(:merge_request, @@ -1425,14 +1435,14 @@ describe MergeRequest do describe "#source_project_missing?" do let(:project) { create(:project) } - let(:fork_project) { create(:project, forked_from_project: project) } + let(:forked_project) { fork_project(project) } let(:user) { create(:user) } - let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } + let(:unlink_project) { Projects::UnlinkForkService.new(forked_project, user) } context "when the fork exists" do let(:merge_request) do create(:merge_request, - source_project: fork_project, + source_project: forked_project, target_project: project) end @@ -1446,9 +1456,9 @@ describe MergeRequest do end context "when the fork does not exist" do - let(:merge_request) do + let!(:merge_request) do create(:merge_request, - source_project: fork_project, + source_project: forked_project, target_project: project) end @@ -1471,14 +1481,14 @@ describe MergeRequest do describe "#closed_without_fork?" do let(:project) { create(:project) } - let(:fork_project) { create(:project, forked_from_project: project) } + let(:forked_project) { fork_project(project) } let(:user) { create(:user) } - let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } + let(:unlink_project) { Projects::UnlinkForkService.new(forked_project, user) } context "when the merge request is closed" do let(:closed_merge_request) do create(:closed_merge_request, - source_project: fork_project, + source_project: forked_project, target_project: project) end @@ -1497,7 +1507,7 @@ describe MergeRequest do context "when the merge request is open" do let(:open_merge_request) do create(:merge_request, - source_project: fork_project, + source_project: forked_project, target_project: project) end @@ -1516,24 +1526,24 @@ describe MergeRequest do end context 'forked project' do - let(:project) { create(:project) } + let(:project) { create(:project, :public) } let(:user) { create(:user) } - let(:fork_project) { create(:project, forked_from_project: project, namespace: user.namespace) } + let(:forked_project) { fork_project(project, user) } let!(:merge_request) do create(:closed_merge_request, - source_project: fork_project, + source_project: forked_project, target_project: project) end it 'returns false if unforked' do - Projects::UnlinkForkService.new(fork_project, user).execute + Projects::UnlinkForkService.new(forked_project, user).execute expect(merge_request.reload.reopenable?).to be_falsey end it 'returns false if the source project is deleted' do - Projects::DestroyService.new(fork_project, user).execute + Projects::DestroyService.new(forked_project, user).execute expect(merge_request.reload.reopenable?).to be_falsey end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 3ea614776ca..2ebf6acd42a 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Namespace do + include ProjectForksHelper + let!(:namespace) { create(:namespace) } describe 'associations' do @@ -520,4 +522,25 @@ describe Namespace do end end end + + describe '#has_forks_of?' do + let(:project) { create(:project, :public) } + let!(:forked_project) { fork_project(project, namespace.owner, namespace: namespace) } + + before do + # Reset the fork network relation + project.reload + end + + it 'knows if there is a direct fork in the namespace' do + expect(namespace.find_fork_of(project)).to eq(forked_project) + end + + it 'knows when there is as fork-of-fork in the namespace' do + other_namespace = create(:namespace) + other_fork = fork_project(forked_project, other_namespace.owner, namespace: other_namespace) + + expect(other_namespace.find_fork_of(project)).to eq(other_fork) + end + end end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index b214074fdce..1ecb50586c7 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -314,6 +314,56 @@ describe Note do expect(subject[active_diff_note1.line_code].first.id).to eq(active_diff_note1.discussion_id) expect(subject[active_diff_note3.line_code].first.id).to eq(active_diff_note3.discussion_id) end + + context 'with image discussions' do + let(:merge_request2) { create(:merge_request_with_diffs, :with_image_diffs, source_project: project, title: "Added images and changes") } + let(:image_path) { "files/images/ee_repo_logo.png" } + let(:text_path) { "bar/branch-test.txt" } + let!(:image_note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request2, position: image_position) } + let!(:text_note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request2, position: text_position) } + + let(:image_position) do + Gitlab::Diff::Position.new( + old_path: image_path, + new_path: image_path, + width: 100, + height: 100, + x: 1, + y: 1, + position_type: "image", + diff_refs: merge_request2.diff_refs + ) + end + + let(:text_position) do + Gitlab::Diff::Position.new( + old_path: text_path, + new_path: text_path, + old_line: nil, + new_line: 2, + position_type: "text", + diff_refs: merge_request2.diff_refs + ) + end + + it "groups image discussions by file identifier" do + diff_discussion = DiffDiscussion.new([image_note]) + + discussions = merge_request2.notes.grouped_diff_discussions + + expect(discussions.size).to eq(2) + expect(discussions[image_note.diff_file.new_path]).to include(diff_discussion) + end + + it "groups text discussions by line code" do + diff_discussion = DiffDiscussion.new([text_note]) + + discussions = merge_request2.notes.grouped_diff_discussions + + expect(discussions.size).to eq(2) + expect(discussions[text_note.line_code]).to include(diff_discussion) + end + end end context 'diff discussions for older diff refs' do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 9b470e79a76..a26c71e5155 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -57,6 +57,7 @@ describe Project do it { is_expected.to have_many(:commit_statuses) } it { is_expected.to have_many(:pipelines) } it { is_expected.to have_many(:builds) } + it { is_expected.to have_many(:build_trace_section_names)} it { is_expected.to have_many(:runner_projects) } it { is_expected.to have_many(:runners) } it { is_expected.to have_many(:active_runners) } @@ -692,6 +693,44 @@ describe Project do project.cache_has_external_issue_tracker end.to change { project.has_external_issue_tracker}.to(false) end + + it 'does not cache data when in a read-only GitLab instance' do + allow(Gitlab::Database).to receive(:read_only?) { true } + + expect do + project.cache_has_external_issue_tracker + end.not_to change { project.has_external_issue_tracker } + end + end + + describe '#cache_has_external_wiki' do + let(:project) { create(:project, has_external_wiki: nil) } + + it 'stores true if there is any external_wikis' do + services = double(:service, external_wikis: [ExternalWikiService.new]) + expect(project).to receive(:services).and_return(services) + + expect do + project.cache_has_external_wiki + end.to change { project.has_external_wiki}.to(true) + end + + it 'stores false if there is no external_wikis' do + services = double(:service, external_wikis: []) + expect(project).to receive(:services).and_return(services) + + expect do + project.cache_has_external_wiki + end.to change { project.has_external_wiki}.to(false) + end + + it 'does not cache data when in a read-only GitLab instance' do + allow(Gitlab::Database).to receive(:read_only?) { true } + + expect do + project.cache_has_external_wiki + end.not_to change { project.has_external_wiki } + end end describe '#has_wiki?' do @@ -1817,6 +1856,59 @@ describe Project do end end + context 'forks' do + include ProjectForksHelper + + let(:project) { create(:project, :public) } + let!(:forked_project) { fork_project(project) } + + describe '#fork_network' do + it 'includes a fork of the project' do + expect(project.fork_network.projects).to include(forked_project) + end + + it 'includes a fork of a fork' do + other_fork = fork_project(forked_project) + + expect(project.fork_network.projects).to include(other_fork) + end + + it 'includes sibling forks' do + other_fork = fork_project(project) + + expect(forked_project.fork_network.projects).to include(other_fork) + end + + it 'includes the base project' do + expect(forked_project.fork_network.projects).to include(project.reload) + end + end + + describe '#in_fork_network_of?' do + it 'is true for a real fork' do + expect(forked_project.in_fork_network_of?(project)).to be_truthy + end + + it 'is true for a fork of a fork', :postgresql do + other_fork = fork_project(forked_project) + + expect(other_fork.in_fork_network_of?(project)).to be_truthy + end + + it 'is true for sibling forks' do + sibling = fork_project(project) + + expect(sibling.in_fork_network_of?(forked_project)).to be_truthy + end + + it 'is false when another project is given' do + other_project = build_stubbed(:project) + + expect(forked_project.in_fork_network_of?(other_project)).to be_falsy + end + end + end + describe '#pushes_since_gc' do let(:project) { create(:project) } @@ -2446,7 +2538,7 @@ describe Project do expect(project.migrate_to_hashed_storage!).to be_truthy end - it 'flags as readonly' do + it 'flags as read-only' do expect { project.migrate_to_hashed_storage! }.to change { project.repository_read_only }.to(true) end @@ -2573,7 +2665,7 @@ describe Project do expect(project.migrate_to_hashed_storage!).to be_nil end - it 'does not flag as readonly' do + it 'does not flag as read-only' do expect { project.migrate_to_hashed_storage! }.not_to change { project.repository_read_only } end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 9f517e4af72..ece6968dde6 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe User do include Gitlab::CurrentSettings + include ProjectForksHelper describe 'modules' do subject { described_class } @@ -1431,7 +1432,7 @@ describe User do describe "#contributed_projects" do subject { create(:user) } let!(:project1) { create(:project) } - let!(:project2) { create(:project, forked_from_project: project3) } + let!(:project2) { fork_project(project3) } let!(:project3) { create(:project) } let!(:merge_request) { create(:merge_request, source_project: project2, target_project: project3, author: subject) } let!(:push_event) { create(:push_event, project: project1, author: subject) } @@ -1455,6 +1456,23 @@ describe User do end end + describe '#fork_of' do + let(:user) { create(:user) } + + it "returns a user's fork of a project" do + project = create(:project, :public) + user_fork = fork_project(project, user, namespace: user.namespace) + + expect(user.fork_of(project)).to eq(user_fork) + end + + it 'returns nil if the project does not have a fork network' do + project = create(:project) + + expect(user.fork_of(project)).to be_nil + end + end + describe '#can_be_removed?' do subject { create(:user) } @@ -2282,4 +2300,49 @@ describe User do end end end + + describe '#confirm_deletion_with_password?' do + where( + password_automatically_set: [true, false], + ldap_user: [true, false], + password_authentication_disabled: [true, false] + ) + + with_them do + let!(:user) { create(:user, password_automatically_set: password_automatically_set) } + let!(:identity) { create(:identity, user: user) if ldap_user } + + # Only confirm deletion with password if all inputs are false + let(:expected) { !(password_automatically_set || ldap_user || password_authentication_disabled) } + + before do + stub_application_setting(password_authentication_enabled: !password_authentication_disabled) + end + + it 'returns false unless all inputs are true' do + expect(user.confirm_deletion_with_password?).to eq(expected) + end + end + end + + describe '#delete_async' do + let(:user) { create(:user) } + let(:deleted_by) { create(:user) } + + it 'blocks the user then schedules them for deletion if a hard delete is specified' do + expect(DeleteUserWorker).to receive(:perform_async).with(deleted_by.id, user.id, hard_delete: true) + + user.delete_async(deleted_by: deleted_by, params: { hard_delete: true }) + + expect(user).to be_blocked + end + + it 'schedules user for deletion without blocking them' do + expect(DeleteUserWorker).to receive(:perform_async).with(deleted_by.id, user.id, {}) + + user.delete_async(deleted_by: deleted_by) + + expect(user).not_to be_blocked + end + end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index c4f6e97b915..5e66e1607ba 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -1,6 +1,8 @@ require "spec_helper" describe API::MergeRequests do + include ProjectForksHelper + let(:base_time) { Time.now } let(:user) { create(:user) } let(:admin) { create(:user, :admin) } @@ -616,17 +618,17 @@ describe API::MergeRequests do context 'forked projects' do let!(:user2) { create(:user) } - let!(:fork_project) { create(:project, forked_from_project: project, namespace: user2.namespace, creator_id: user2.id) } + let!(:forked_project) { fork_project(project, user2) } let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) } before do - fork_project.add_reporter(user2) + forked_project.add_reporter(user2) allow_any_instance_of(MergeRequest).to receive(:write_ref) end it "returns merge_request" do - post api("/projects/#{fork_project.id}/merge_requests", user2), + post api("/projects/#{forked_project.id}/merge_requests", user2), title: 'Test merge_request', source_branch: "feature_conflict", target_branch: "master", author: user2, target_project_id: project.id, description: 'Test description for Test merge_request' expect(response).to have_gitlab_http_status(201) @@ -635,10 +637,10 @@ describe API::MergeRequests do end it "does not return 422 when source_branch equals target_branch" do - expect(project.id).not_to eq(fork_project.id) - expect(fork_project.forked?).to be_truthy - expect(fork_project.forked_from_project).to eq(project) - post api("/projects/#{fork_project.id}/merge_requests", user2), + expect(project.id).not_to eq(forked_project.id) + expect(forked_project.forked?).to be_truthy + expect(forked_project.forked_from_project).to eq(project) + post api("/projects/#{forked_project.id}/merge_requests", user2), title: 'Test merge_request', source_branch: "master", target_branch: "master", author: user2, target_project_id: project.id expect(response).to have_gitlab_http_status(201) expect(json_response['title']).to eq('Test merge_request') @@ -647,7 +649,7 @@ describe API::MergeRequests do it 'returns 422 when target project has disabled merge requests' do project.project_feature.update(merge_requests_access_level: 0) - post api("/projects/#{fork_project.id}/merge_requests", user2), + post api("/projects/#{forked_project.id}/merge_requests", user2), title: 'Test', target_branch: 'master', source_branch: 'markdown', @@ -658,36 +660,26 @@ describe API::MergeRequests do end it "returns 400 when source_branch is missing" do - post api("/projects/#{fork_project.id}/merge_requests", user2), + post api("/projects/#{forked_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id expect(response).to have_gitlab_http_status(400) end it "returns 400 when target_branch is missing" do - post api("/projects/#{fork_project.id}/merge_requests", user2), + post api("/projects/#{forked_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id expect(response).to have_gitlab_http_status(400) end it "returns 400 when title is missing" do - post api("/projects/#{fork_project.id}/merge_requests", user2), + post api("/projects/#{forked_project.id}/merge_requests", user2), target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: project.id expect(response).to have_gitlab_http_status(400) end context 'when target_branch is specified' do - it 'returns 422 if not a forked project' do - post api("/projects/#{project.id}/merge_requests", user), - title: 'Test merge_request', - target_branch: 'master', - source_branch: 'markdown', - author: user, - target_project_id: fork_project.id - expect(response).to have_gitlab_http_status(422) - end - it 'returns 422 if targeting a different fork' do - post api("/projects/#{fork_project.id}/merge_requests", user2), + post api("/projects/#{forked_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', @@ -698,8 +690,8 @@ describe API::MergeRequests do end it "returns 201 when target_branch is specified and for the same project" do - post api("/projects/#{fork_project.id}/merge_requests", user2), - title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: fork_project.id + post api("/projects/#{forked_project.id}/merge_requests", user2), + title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: forked_project.id expect(response).to have_gitlab_http_status(201) end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 18f6f7df1fa..5964244f8c5 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -64,9 +64,12 @@ describe API::Projects do create(:project, :public) end + # TODO: We're currently querying to detect if a project is a fork + # in 2 ways. Lower this back to 8 when `ForkedProjectLink` relation is + # removed expect do get api('/projects', current_user) - end.not_to exceed_query_limit(control).with_threshold(8) + end.not_to exceed_query_limit(control).with_threshold(9) end end diff --git a/spec/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb index 86f38dd4ec1..df73c731c96 100644 --- a/spec/requests/api/v3/merge_requests_spec.rb +++ b/spec/requests/api/v3/merge_requests_spec.rb @@ -1,6 +1,8 @@ require "spec_helper" describe API::MergeRequests do + include ProjectForksHelper + let(:base_time) { Time.now } let(:user) { create(:user) } let(:admin) { create(:user, :admin) } @@ -312,17 +314,17 @@ describe API::MergeRequests do context 'forked projects' do let!(:user2) { create(:user) } - let!(:fork_project) { create(:project, forked_from_project: project, namespace: user2.namespace, creator_id: user2.id) } + let!(:forked_project) { fork_project(project, user2) } let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) } before do - fork_project.add_reporter(user2) + forked_project.add_reporter(user2) allow_any_instance_of(MergeRequest).to receive(:write_ref) end it "returns merge_request" do - post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + post v3_api("/projects/#{forked_project.id}/merge_requests", user2), title: 'Test merge_request', source_branch: "feature_conflict", target_branch: "master", author: user2, target_project_id: project.id, description: 'Test description for Test merge_request' expect(response).to have_gitlab_http_status(201) @@ -331,10 +333,10 @@ describe API::MergeRequests do end it "does not return 422 when source_branch equals target_branch" do - expect(project.id).not_to eq(fork_project.id) - expect(fork_project.forked?).to be_truthy - expect(fork_project.forked_from_project).to eq(project) - post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + expect(project.id).not_to eq(forked_project.id) + expect(forked_project.forked?).to be_truthy + expect(forked_project.forked_from_project).to eq(project) + post v3_api("/projects/#{forked_project.id}/merge_requests", user2), title: 'Test merge_request', source_branch: "master", target_branch: "master", author: user2, target_project_id: project.id expect(response).to have_gitlab_http_status(201) expect(json_response['title']).to eq('Test merge_request') @@ -343,7 +345,7 @@ describe API::MergeRequests do it "returns 422 when target project has disabled merge requests" do project.project_feature.update(merge_requests_access_level: 0) - post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + post v3_api("/projects/#{forked_project.id}/merge_requests", user2), title: 'Test', target_branch: "master", source_branch: 'markdown', @@ -354,36 +356,26 @@ describe API::MergeRequests do end it "returns 400 when source_branch is missing" do - post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + post v3_api("/projects/#{forked_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id expect(response).to have_gitlab_http_status(400) end it "returns 400 when target_branch is missing" do - post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + post v3_api("/projects/#{forked_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id expect(response).to have_gitlab_http_status(400) end it "returns 400 when title is missing" do - post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + post v3_api("/projects/#{forked_project.id}/merge_requests", user2), target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: project.id expect(response).to have_gitlab_http_status(400) end context 'when target_branch is specified' do - it 'returns 422 if not a forked project' do - post v3_api("/projects/#{project.id}/merge_requests", user), - title: 'Test merge_request', - target_branch: 'master', - source_branch: 'markdown', - author: user, - target_project_id: fork_project.id - expect(response).to have_gitlab_http_status(422) - end - it 'returns 422 if targeting a different fork' do - post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + post v3_api("/projects/#{forked_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', @@ -394,8 +386,8 @@ describe API::MergeRequests do end it "returns 201 when target_branch is specified and for the same project" do - post v3_api("/projects/#{fork_project.id}/merge_requests", user2), - title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: fork_project.id + post v3_api("/projects/#{forked_project.id}/merge_requests", user2), + title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: forked_project.id expect(response).to have_gitlab_http_status(201) end end diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb index 27d09b8202e..bca5bf81c5c 100644 --- a/spec/requests/lfs_http_spec.rb +++ b/spec/requests/lfs_http_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe 'Git LFS API and storage' do include WorkhorseHelpers + include ProjectForksHelper let(:user) { create(:user) } let!(:lfs_object) { create(:lfs_object, :with_file) } @@ -824,6 +825,34 @@ describe 'Git LFS API and storage' do end end + describe 'when handling lfs batch request on a read-only GitLab instance' do + let(:authorization) { authorize_user } + let(:project) { create(:project) } + let(:path) { "#{project.http_url_to_repo}/info/lfs/objects/batch" } + let(:body) do + { 'objects' => [{ 'oid' => sample_oid, 'size' => sample_size }] } + end + + before do + allow(Gitlab::Database).to receive(:read_only?) { true } + project.team << [user, :master] + enable_lfs + end + + it 'responds with a 200 message on download' do + post_lfs_json path, body.merge('operation' => 'download'), headers + + expect(response).to have_gitlab_http_status(200) + end + + it 'responds with a 403 message on upload' do + post_lfs_json path, body.merge('operation' => 'upload'), headers + + expect(response).to have_gitlab_http_status(403) + expect(json_response).to include('message' => 'You cannot write to this read-only GitLab instance.') + end + end + describe 'when pushing a lfs object' do before do enable_lfs @@ -1173,11 +1202,6 @@ describe 'Git LFS API and storage' do ActionController::HttpAuthentication::Basic.encode_credentials(user.username, Gitlab::LfsToken.new(user).token) end - def fork_project(project, user, object = nil) - allow(RepositoryForkWorker).to receive(:perform_async).and_return(true) - Projects::ForkService.new(project, user, {}).execute - end - def post_lfs_json(url, body = nil, headers = nil) post(url, body.try(:to_json), (headers || {}).merge('Content-Type' => 'application/vnd.git-lfs+json')) end diff --git a/spec/serializers/build_details_entity_spec.rb b/spec/serializers/build_details_entity_spec.rb index 5b7822d5d8e..f6bd6e9ede4 100644 --- a/spec/serializers/build_details_entity_spec.rb +++ b/spec/serializers/build_details_entity_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe BuildDetailsEntity do + include ProjectForksHelper + set(:user) { create(:admin) } it 'inherits from JobEntity' do @@ -56,18 +58,16 @@ describe BuildDetailsEntity do end context 'when merge request is from a fork' do - let(:fork_project) do - create(:project, forked_from_project: project) - end + let(:forked_project) { fork_project(project) } - let(:pipeline) { create(:ci_pipeline, project: fork_project) } + let(:pipeline) { create(:ci_pipeline, project: forked_project) } before do allow(build).to receive(:merge_request).and_return(merge_request) end let(:merge_request) do - create(:merge_request, source_project: fork_project, + create(:merge_request, source_project: forked_project, target_project: project, source_branch: build.ref) end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index eb6e683cc23..08847183bf4 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Ci::CreatePipelineService do + include ProjectForksHelper + set(:project) { create(:project, :repository) } let(:user) { create(:admin) } let(:ref_name) { 'refs/heads/master' } @@ -82,13 +84,9 @@ describe Ci::CreatePipelineService do end context 'when merge request target project is different from source project' do + let!(:project) { fork_project(target_project, nil, repository: true) } let!(:target_project) { create(:project, :repository) } - let!(:forked_project_link) do - create(:forked_project_link, forked_to_project: project, - forked_from_project: target_project) - end - it 'updates head pipeline for merge request' do merge_request = create(:merge_request, source_branch: 'master', target_branch: "branch_1", diff --git a/spec/services/ci/extract_sections_from_build_trace_service_spec.rb b/spec/services/ci/extract_sections_from_build_trace_service_spec.rb new file mode 100644 index 00000000000..28f2fa7903a --- /dev/null +++ b/spec/services/ci/extract_sections_from_build_trace_service_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe Ci::ExtractSectionsFromBuildTraceService, '#execute' do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:build) { create(:ci_build, project: project) } + + subject { described_class.new(project, user) } + + shared_examples 'build trace has sections markers' do + before do + build.trace.set(File.read(expand_fixture_path('trace/trace_with_sections'))) + end + + it 'saves the correct extracted sections' do + expect(build.trace_sections).to be_empty + expect(subject.execute(build)).to be(true) + expect(build.trace_sections).not_to be_empty + end + + it "fails if trace_sections isn't empty" do + expect(subject.execute(build)).to be(true) + expect(build.trace_sections).not_to be_empty + + expect(subject.execute(build)).to be(false) + expect(build.trace_sections).not_to be_empty + end + end + + shared_examples 'build trace has no sections markers' do + before do + build.trace.set('no markerts') + end + + it 'extracts no sections' do + expect(build.trace_sections).to be_empty + expect(subject.execute(build)).to be(true) + expect(build.trace_sections).to be_empty + end + end + + context 'when the build has no user' do + it_behaves_like 'build trace has sections markers' + it_behaves_like 'build trace has no sections markers' + end + + context 'when the build has a valid user' do + before do + build.user = user + end + + it_behaves_like 'build trace has sections markers' + it_behaves_like 'build trace has no sections markers' + end +end diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb index fbb3213f42b..9db3568abee 100644 --- a/spec/services/ci/retry_build_service_spec.rb +++ b/spec/services/ci/retry_build_service_spec.rb @@ -20,7 +20,7 @@ describe Ci::RetryBuildService do erased_at auto_canceled_by].freeze IGNORE_ACCESSORS = - %i[type lock_version target_url base_tags + %i[type lock_version target_url base_tags trace_sections commit_id deployments erased_by_id last_deployment project_id runner_id tag_taggings taggings tags trigger_request_id user_id auto_canceled_by_id retried failure_reason].freeze diff --git a/spec/services/delete_merged_branches_service_spec.rb b/spec/services/delete_merged_branches_service_spec.rb index 03c682ae0d7..5a9eb359ee1 100644 --- a/spec/services/delete_merged_branches_service_spec.rb +++ b/spec/services/delete_merged_branches_service_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe DeleteMergedBranchesService do + include ProjectForksHelper + subject(:service) { described_class.new(project, project.owner) } let(:project) { create(:project, :repository) } @@ -50,9 +52,9 @@ describe DeleteMergedBranchesService do context 'open merge requests' do it 'does not delete branches from open merge requests' do - fork_link = create(:forked_project_link, forked_from_project: project) + forked_project = fork_project(project) create(:merge_request, :opened, source_project: project, target_project: project, source_branch: 'branch-merged', target_branch: 'master') - create(:merge_request, :opened, source_project: fork_link.forked_to_project, target_project: project, target_branch: 'improve/awesome', source_branch: 'master') + create(:merge_request, :opened, source_project: forked_project, target_project: project, target_branch: 'improve/awesome', source_branch: 'master') service.execute diff --git a/spec/services/discussions/update_diff_position_service_spec.rb b/spec/services/discussions/update_diff_position_service_spec.rb index 82b156f5ebe..2b84206318f 100644 --- a/spec/services/discussions/update_diff_position_service_spec.rb +++ b/spec/services/discussions/update_diff_position_service_spec.rb @@ -164,8 +164,8 @@ describe Discussions::UpdateDiffPositionService do change_position = discussion.change_position expect(change_position.start_sha).to eq(old_diff_refs.head_sha) expect(change_position.head_sha).to eq(new_diff_refs.head_sha) - expect(change_position.old_line).to eq(9) - expect(change_position.new_line).to be_nil + expect(change_position.formatter.old_line).to eq(9) + expect(change_position.formatter.new_line).to be_nil end it 'creates a system discussion' do @@ -184,7 +184,7 @@ describe Discussions::UpdateDiffPositionService do expect(discussion.original_position).to eq(old_position) expect(discussion.position).not_to eq(old_position) - expect(discussion.position.new_line).to eq(22) + expect(discussion.position.formatter.new_line).to eq(22) end context 'when the resolve_outdated_diff_discussions setting is set' do diff --git a/spec/services/merge_requests/conflicts/resolve_service_spec.rb b/spec/services/merge_requests/conflicts/resolve_service_spec.rb index 6f49a65d795..9c9b0c4c4a1 100644 --- a/spec/services/merge_requests/conflicts/resolve_service_spec.rb +++ b/spec/services/merge_requests/conflicts/resolve_service_spec.rb @@ -1,14 +1,16 @@ require 'spec_helper' describe MergeRequests::Conflicts::ResolveService do + include ProjectForksHelper let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - - let(:fork_project) do - create(:forked_project_with_submodules) do |fork_project| - fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id) - fork_project.save - end + let(:project) { create(:project, :public, :repository) } + + let(:forked_project) do + forked_project = fork_project(project, user) + TestEnv.copy_repo(forked_project, + bare_repo: TestEnv.forked_repo_path_bare, + refs: TestEnv::FORKED_BRANCH_SHA) + forked_project end let(:merge_request) do @@ -19,7 +21,7 @@ describe MergeRequests::Conflicts::ResolveService do let(:merge_request_from_fork) do create(:merge_request, - source_branch: 'conflict-resolvable-fork', source_project: fork_project, + source_branch: 'conflict-resolvable-fork', source_project: forked_project, target_branch: 'conflict-start', target_project: project) end @@ -114,7 +116,7 @@ describe MergeRequests::Conflicts::ResolveService do end it 'gets conflicts from the source project' do - expect(fork_project.repository.rugged).to receive(:merge_commits).and_call_original + expect(forked_project.repository.rugged).to receive(:merge_commits).and_call_original expect(project.repository.rugged).not_to receive(:merge_commits) resolve_conflicts diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb index 25599dea19f..274624aa8bb 100644 --- a/spec/services/merge_requests/get_urls_service_spec.rb +++ b/spec/services/merge_requests/get_urls_service_spec.rb @@ -1,6 +1,8 @@ require "spec_helper" describe MergeRequests::GetUrlsService do + include ProjectForksHelper + let(:project) { create(:project, :public, :repository) } let(:service) { described_class.new(project) } let(:source_branch) { "merge-test" } @@ -85,7 +87,7 @@ describe MergeRequests::GetUrlsService do context 'pushing to existing branch from forked project' do let(:user) { create(:user) } - let!(:forked_project) { Projects::ForkService.new(project, user).execute } + let!(:forked_project) { fork_project(project, user, repository: true) } let!(:merge_request) { create(:merge_request, source_project: forked_project, target_project: project, source_branch: source_branch) } let(:changes) { existing_branch_changes } # Source project is now the forked one diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 64e676f22a0..62dbe362ec8 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe MergeRequests::RefreshService do + include ProjectForksHelper + let(:project) { create(:project, :repository) } let(:user) { create(:user) } let(:service) { described_class } @@ -12,7 +14,8 @@ describe MergeRequests::RefreshService do group.add_owner(@user) @project = create(:project, :repository, namespace: group) - @fork_project = Projects::ForkService.new(@project, @user).execute + @fork_project = fork_project(@project, @user, repository: true) + @merge_request = create(:merge_request, source_project: @project, source_branch: 'master', @@ -311,8 +314,7 @@ describe MergeRequests::RefreshService do context 'when the merge request is sourced from a different project' do it 'creates a `MergeRequestsClosingIssues` record for each issue closed by a commit' do - forked_project = create(:project, :repository) - create(:forked_project_link, forked_to_project: forked_project, forked_from_project: @project) + forked_project = fork_project(@project, @user, repository: true) merge_request = create(:merge_request, target_branch: 'master', diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index c867139d1de..c90bad46295 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -212,6 +212,19 @@ describe Projects::DestroyService do end end + context 'as the root of a fork network' do + let!(:fork_network) { create(:fork_network, root_project: project) } + + it 'updates the fork network with the project name' do + destroy_project(project, user) + + fork_network.reload + + expect(fork_network.deleted_root_project_name).to eq(project.full_name) + expect(fork_network.root_project).to be_nil + end + end + def destroy_project(project, user, params = {}) if async Projects::DestroyService.new(project, user, params).async_execute diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb index fa9d6969830..53862283a27 100644 --- a/spec/services/projects/fork_service_spec.rb +++ b/spec/services/projects/fork_service_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe Projects::ForkService do + include ProjectForksHelper let(:gitlab_shell) { Gitlab::Shell.new } describe 'fork by user' do @@ -33,7 +34,7 @@ describe Projects::ForkService do end describe "successfully creates project in the user namespace" do - let(:to_project) { fork_project(@from_project, @to_user) } + let(:to_project) { fork_project(@from_project, @to_user, namespace: @to_user.namespace) } it { expect(to_project).to be_persisted } it { expect(to_project.errors).to be_empty } @@ -60,13 +61,40 @@ describe Projects::ForkService do expect(@from_project.forks_count).to eq(1) end + + it 'creates a fork network with the new project and the root project set' do + to_project + fork_network = @from_project.reload.fork_network + + expect(fork_network).not_to be_nil + expect(fork_network.root_project).to eq(@from_project) + expect(fork_network.projects).to contain_exactly(@from_project, to_project) + end + end + + context 'creating a fork of a fork' do + let(:from_forked_project) { fork_project(@from_project, @to_user) } + let(:other_namespace) do + group = create(:group) + group.add_owner(@to_user) + group + end + let(:to_project) { fork_project(from_forked_project, @to_user, namespace: other_namespace) } + + it 'sets the root of the network to the root project' do + expect(to_project.fork_network.root_project).to eq(@from_project) + end + + it 'sets the forked_from_project on the membership' do + expect(to_project.fork_network_member.forked_from_project).to eq(from_forked_project) + end end end context 'project already exists' do it "fails due to validation, not transaction failure" do @existing_project = create(:project, :repository, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace) - @to_project = fork_project(@from_project, @to_user) + @to_project = fork_project(@from_project, @to_user, namespace: @to_namespace) expect(@existing_project).to be_persisted expect(@to_project).not_to be_persisted @@ -88,7 +116,7 @@ describe Projects::ForkService do end it 'does not allow creation' do - to_project = fork_project(@from_project, @to_user) + to_project = fork_project(@from_project, @to_user, namespace: @to_user.namespace) expect(to_project).not_to be_persisted expect(to_project.errors.messages).to have_key(:base) @@ -182,9 +210,4 @@ describe Projects::ForkService do end end end - - def fork_project(from_project, user, params = {}) - allow(RepositoryForkWorker).to receive(:perform_async).and_return(true) - Projects::ForkService.new(from_project, user, params).execute - end end diff --git a/spec/services/projects/hashed_storage_migration_service_spec.rb b/spec/services/projects/hashed_storage_migration_service_spec.rb index 1b61207b550..aa1988d29d6 100644 --- a/spec/services/projects/hashed_storage_migration_service_spec.rb +++ b/spec/services/projects/hashed_storage_migration_service_spec.rb @@ -20,7 +20,7 @@ describe Projects::HashedStorageMigrationService do expect(gitlab_shell.exists?(project.repository_storage_path, "#{hashed_storage.disk_path}.wiki.git")).to be_truthy end - it 'updates project to be hashed and not readonly' do + it 'updates project to be hashed and not read-only' do service.execute expect(project.hashed_storage?).to be_truthy diff --git a/spec/services/projects/unlink_fork_service_spec.rb b/spec/services/projects/unlink_fork_service_spec.rb index 4f1ab697460..50d3a4ec982 100644 --- a/spec/services/projects/unlink_fork_service_spec.rb +++ b/spec/services/projects/unlink_fork_service_spec.rb @@ -1,19 +1,22 @@ require 'spec_helper' describe Projects::UnlinkForkService do - subject { described_class.new(fork_project, user) } + include ProjectForksHelper - let(:fork_link) { create(:forked_project_link) } - let(:fork_project) { fork_link.forked_to_project } + subject { described_class.new(forked_project, user) } + + let(:fork_link) { forked_project.forked_project_link } + let(:project) { create(:project, :public) } + let(:forked_project) { fork_project(project, user) } let(:user) { create(:user) } context 'with opened merge request on the source project' do - let(:merge_request) { create(:merge_request, source_project: fork_project, target_project: fork_link.forked_from_project) } - let(:mr_close_service) { MergeRequests::CloseService.new(fork_project, user) } + let(:merge_request) { create(:merge_request, source_project: forked_project, target_project: fork_link.forked_from_project) } + let(:mr_close_service) { MergeRequests::CloseService.new(forked_project, user) } before do allow(MergeRequests::CloseService).to receive(:new) - .with(fork_project, user) + .with(forked_project, user) .and_return(mr_close_service) end @@ -25,13 +28,24 @@ describe Projects::UnlinkForkService do end it 'remove fork relation' do - expect(fork_project.forked_project_link).to receive(:destroy) + expect(forked_project.forked_project_link).to receive(:destroy) + + subject.execute + end + + it 'removes the link to the fork network' do + expect(forked_project.fork_network_member).to be_present + expect(forked_project.fork_network).to be_present subject.execute + forked_project.reload + + expect(forked_project.fork_network_member).to be_nil + expect(forked_project.reload.fork_network).to be_nil end it 'refreshes the forks count cache of the source project' do - source = fork_project.forked_from_project + source = forked_project.forked_from_project expect(source.forks_count).to eq(1) diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index d400304622e..3da222e2ed8 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Projects::UpdateService, '#execute' do + include ProjectForksHelper + let(:gitlab_shell) { Gitlab::Shell.new } let(:user) { create(:user) } let(:admin) { create(:admin) } @@ -76,13 +78,7 @@ describe Projects::UpdateService, '#execute' do describe 'when updating project that has forks' do let(:project) { create(:project, :internal) } - let(:forked_project) { create(:forked_project_with_submodules, :internal) } - - before do - forked_project.build_forked_project_link(forked_to_project_id: forked_project.id, - forked_from_project_id: project.id) - forked_project.save - end + let(:forked_project) { fork_project(project) } it 'updates forks visibility level when parent set to more restrictive' do opts = { visibility_level: Gitlab::VisibilityLevel::PRIVATE } diff --git a/spec/services/users/activity_service_spec.rb b/spec/services/users/activity_service_spec.rb index fef4da0c76e..17eabad73be 100644 --- a/spec/services/users/activity_service_spec.rb +++ b/spec/services/users/activity_service_spec.rb @@ -38,6 +38,18 @@ describe Users::ActivityService do end end end + + context 'when in GitLab read-only instance' do + before do + allow(Gitlab::Database).to receive(:read_only?).and_return(true) + end + + it 'does not update last_activity_at' do + service.execute + + expect(last_hour_user_ids).to eq([]) + end + end end def last_hour_user_ids diff --git a/spec/support/features/discussion_comments_shared_example.rb b/spec/support/features/discussion_comments_shared_example.rb index 81cb94ab8c4..9f05cabf7ae 100644 --- a/spec/support/features/discussion_comments_shared_example.rb +++ b/spec/support/features/discussion_comments_shared_example.rb @@ -71,7 +71,7 @@ shared_examples 'discussion comments' do |resource_name| expect(page).not_to have_selector menu_selector find(toggle_selector).click - find('body').click + find('body').trigger 'click' expect(page).not_to have_selector menu_selector end diff --git a/spec/support/project_forks_helper.rb b/spec/support/project_forks_helper.rb new file mode 100644 index 00000000000..0d1c6792d13 --- /dev/null +++ b/spec/support/project_forks_helper.rb @@ -0,0 +1,58 @@ +module ProjectForksHelper + def fork_project(project, user = nil, params = {}) + # Load the `fork_network` for the project to fork as there might be one that + # wasn't loaded yet. + project.reload unless project.fork_network + + unless user + user = create(:user) + project.add_developer(user) + end + + unless params[:namespace] || params[:namespace_id] + params[:namespace] = create(:group) + params[:namespace].add_owner(user) + end + + service = Projects::ForkService.new(project, user, params) + + create_repository = params.delete(:repository) + # Avoid creating a repository + unless create_repository + allow(RepositoryForkWorker).to receive(:perform_async).and_return(true) + shell = double('gitlab_shell', fork_repository: true) + allow(service).to receive(:gitlab_shell).and_return(shell) + end + + forked_project = service.execute + + # Reload the both projects so they know about their newly created fork_network + if forked_project.persisted? + project.reload + forked_project.reload + end + + if create_repository + # The call to project.repository.after_import in RepositoryForkWorker does + # not reset the @exists variable of this forked_project.repository + # so we have to explicitely call this method to clear the @exists variable. + # of the instance we're returning here. + forked_project.repository.after_import + + # We can't leave the hooks in place after a fork, as those would fail in tests + # The "internal" API is not available + FileUtils.rm_rf("#{forked_project.repository.path}/hooks") + end + + forked_project + end + + def fork_project_with_submodules(project, user = nil, params = {}) + forked_project = fork_project(project, user, params) + TestEnv.copy_repo(forked_project, + bare_repo: TestEnv.forked_repo_path_bare, + refs: TestEnv::FORKED_BRANCH_SHA) + + forked_project + end +end diff --git a/spec/support/shared_examples/position_formatters.rb b/spec/support/shared_examples/position_formatters.rb new file mode 100644 index 00000000000..ffc9456dbc7 --- /dev/null +++ b/spec/support/shared_examples/position_formatters.rb @@ -0,0 +1,43 @@ +shared_examples_for "position formatter" do + let(:formatter) { described_class.new(attrs) } + + describe '#key' do + let(:key) { [123, 456, 789, Digest::SHA1.hexdigest(formatter.old_path), Digest::SHA1.hexdigest(formatter.new_path), 1, 2] } + + subject { formatter.key } + + it { is_expected.to eq(key) } + end + + describe '#complete?' do + subject { formatter.complete? } + + context 'when there are missing key attributes' do + it { is_expected.to be_truthy } + end + + context 'when old_line and new_line are nil' do + let(:attrs) { base_attrs } + + it { is_expected.to be_falsy } + end + end + + describe '#to_h' do + let(:formatter_hash) do + attrs.merge(position_type: base_attrs[:position_type] || 'text' ) + end + + subject { formatter.to_h } + + it { is_expected.to eq(formatter_hash) } + end + + describe '#==' do + subject { formatter } + + let(:other_formatter) { described_class.new(attrs) } + + it { is_expected.to eq(other_formatter) } + end +end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 79395f4c564..a27bfdee3d2 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -46,7 +46,8 @@ module TestEnv 'v1.1.0' => 'b83d6e3', 'add-ipython-files' => '93ee732', 'add-pdf-file' => 'e774ebd', - 'add-pdf-text-binary' => '79faa7b' + 'add-pdf-text-binary' => '79faa7b', + 'add_images_and_changes' => '010d106' }.freeze # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily diff --git a/spec/views/projects/merge_requests/_commits.html.haml_spec.rb b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb index 98c7de9b709..efed2e02a1b 100644 --- a/spec/views/projects/merge_requests/_commits.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb @@ -2,10 +2,11 @@ require 'spec_helper' describe 'projects/merge_requests/_commits.html.haml' do include Devise::Test::ControllerHelpers + include ProjectForksHelper let(:user) { create(:user) } - let(:target_project) { create(:project, :repository) } - let(:source_project) { create(:project, :repository, forked_from_project: target_project) } + let(:target_project) { create(:project, :public, :repository) } + let(:source_project) { fork_project(target_project, user, repository: true) } let(:merge_request) do create(:merge_request, :simple, diff --git a/spec/views/projects/merge_requests/edit.html.haml_spec.rb b/spec/views/projects/merge_requests/edit.html.haml_spec.rb index 69c7d0cbf28..9b74a7e1946 100644 --- a/spec/views/projects/merge_requests/edit.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/edit.html.haml_spec.rb @@ -2,16 +2,19 @@ require 'spec_helper' describe 'projects/merge_requests/edit.html.haml' do include Devise::Test::ControllerHelpers + include ProjectForksHelper let(:user) { create(:user) } let(:project) { create(:project, :repository) } - let(:fork_project) { create(:project, :repository, forked_from_project: project) } - let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } + let(:forked_project) { fork_project(project, user, repository: true) } + let(:unlink_project) { Projects::UnlinkForkService.new(forked_project, user) } let(:milestone) { create(:milestone, project: project) } let(:closed_merge_request) do + project.add_developer(user) + create(:closed_merge_request, - source_project: fork_project, + source_project: forked_project, target_project: project, author: user, assignee: user, diff --git a/spec/views/projects/merge_requests/show.html.haml_spec.rb b/spec/views/projects/merge_requests/show.html.haml_spec.rb index 6f29d12373a..28d54c2fb77 100644 --- a/spec/views/projects/merge_requests/show.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb @@ -2,16 +2,17 @@ require 'spec_helper' describe 'projects/merge_requests/show.html.haml' do include Devise::Test::ControllerHelpers + include ProjectForksHelper let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - let(:fork_project) { create(:project, :repository, forked_from_project: project) } - let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } + let(:project) { create(:project, :public, :repository) } + let(:forked_project) { fork_project(project, user, repository: true) } + let(:unlink_project) { Projects::UnlinkForkService.new(forked_project, user) } let(:note) { create(:note_on_merge_request, project: project, noteable: closed_merge_request) } let(:closed_merge_request) do create(:closed_merge_request, - source_project: fork_project, + source_project: forked_project, target_project: project, author: user) end @@ -52,7 +53,7 @@ describe 'projects/merge_requests/show.html.haml' do context 'when the merge request is open' do it 'closes the merge request if the source project does not exist' do closed_merge_request.update_attributes(state: 'open') - fork_project.destroy + forked_project.destroy render diff --git a/spec/workers/build_finished_worker_spec.rb b/spec/workers/build_finished_worker_spec.rb index 8cc3f37ebe8..1a7ffd5cdbf 100644 --- a/spec/workers/build_finished_worker_spec.rb +++ b/spec/workers/build_finished_worker_spec.rb @@ -11,6 +11,8 @@ describe BuildFinishedWorker do expect(BuildHooksWorker) .to receive(:new).ordered.and_call_original + expect(BuildTraceSectionsWorker) + .to receive(:perform_async) expect_any_instance_of(BuildCoverageWorker) .to receive(:perform) expect_any_instance_of(BuildHooksWorker) diff --git a/spec/workers/build_trace_sections_worker_spec.rb b/spec/workers/build_trace_sections_worker_spec.rb new file mode 100644 index 00000000000..45243f45547 --- /dev/null +++ b/spec/workers/build_trace_sections_worker_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe BuildTraceSectionsWorker do + describe '#perform' do + context 'when build exists' do + let!(:build) { create(:ci_build) } + + it 'updates trace sections' do + expect_any_instance_of(Ci::Build) + .to receive(:parse_trace_sections!) + + described_class.new.perform(build.id) + end + end + + context 'when build does not exist' do + it 'does not raise exception' do + expect { described_class.new.perform(123) } + .not_to raise_error + end + end + end +end diff --git a/spec/workers/namespaceless_project_destroy_worker_spec.rb b/spec/workers/namespaceless_project_destroy_worker_spec.rb index 20cf580af8a..ed8cedc0079 100644 --- a/spec/workers/namespaceless_project_destroy_worker_spec.rb +++ b/spec/workers/namespaceless_project_destroy_worker_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe NamespacelessProjectDestroyWorker do + include ProjectForksHelper + subject { described_class.new } before do @@ -55,9 +57,11 @@ describe NamespacelessProjectDestroyWorker do context 'project forked from another' do let!(:parent_project) { create(:project) } - - before do - create(:forked_project_link, forked_to_project: project, forked_from_project: parent_project) + let(:project) do + namespaceless_project = fork_project(parent_project) + namespaceless_project.namespace_id = nil + namespaceless_project.save(validate: false) + namespaceless_project end it 'closes open merge requests' do |