summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb7
-rw-r--r--spec/controllers/projects/merge_requests/diffs_controller_spec.rb10
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb18
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb20
-rw-r--r--spec/controllers/projects_controller_spec.rb20
-rw-r--r--spec/controllers/registrations_controller_spec.rb64
-rw-r--r--spec/factories/ci/build_trace_section_names.rb6
-rw-r--r--spec/factories/fork_networks.rb5
-rw-r--r--spec/factories/merge_requests.rb5
-rw-r--r--spec/factories/projects.rb2
-rw-r--r--spec/features/dashboard/merge_requests_spec.rb3
-rw-r--r--spec/features/issues/issue_detail_spec.rb3
-rw-r--r--spec/features/merge_requests/create_new_mr_from_fork_spec.rb89
-rw-r--r--spec/features/merge_requests/created_from_fork_spec.rb17
-rw-r--r--spec/features/merge_requests/diffs_spec.rb12
-rw-r--r--spec/features/merge_requests/form_spec.rb12
-rw-r--r--spec/features/merge_requests/image_diff_notes.rb196
-rw-r--r--spec/features/merge_requests/user_posts_diff_notes_spec.rb1
-rw-r--r--spec/features/profile_spec.rb44
-rw-r--r--spec/features/projects/issuable_templates_spec.rb8
-rw-r--r--spec/features/projects/user_creates_directory_spec.rb2
-rw-r--r--spec/features/projects/user_creates_files_spec.rb2
-rw-r--r--spec/features/projects/user_deletes_files_spec.rb2
-rw-r--r--spec/features/projects/user_edits_files_spec.rb32
-rw-r--r--spec/features/projects/user_replaces_files_spec.rb2
-rw-r--r--spec/features/projects/user_uploads_files_spec.rb11
-rw-r--r--spec/features/projects_spec.rb56
-rw-r--r--spec/features/protected_branches_spec.rb26
-rw-r--r--spec/finders/merge_request_target_project_finder_spec.rb54
-rw-r--r--spec/finders/merge_requests_finder_spec.rb12
-rw-r--r--spec/fixtures/trace/trace_with_sections15
-rw-r--r--spec/helpers/merge_requests_helper_spec.rb7
-rw-r--r--spec/javascripts/image_diff/helpers/badge_helper_spec.js132
-rw-r--r--spec/javascripts/image_diff/helpers/comment_indicator_helper_spec.js139
-rw-r--r--spec/javascripts/image_diff/helpers/dom_helper_spec.js118
-rw-r--r--spec/javascripts/image_diff/helpers/utils_helper_spec.js207
-rw-r--r--spec/javascripts/image_diff/image_badge_spec.js84
-rw-r--r--spec/javascripts/image_diff/image_diff_spec.js361
-rw-r--r--spec/javascripts/image_diff/init_discussion_tab_spec.js37
-rw-r--r--spec/javascripts/image_diff/mock_data.js28
-rw-r--r--spec/javascripts/image_diff/replaced_image_diff_spec.js312
-rw-r--r--spec/javascripts/image_diff/view_types_spec.js24
-rw-r--r--spec/javascripts/lib/utils/image_utility_spec.js32
-rw-r--r--spec/javascripts/profile/account/components/delete_account_modal_spec.js129
-rw-r--r--spec/lib/banzai/renderer_spec.rb9
-rw-r--r--spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb117
-rw-r--r--spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb85
-rw-r--r--spec/lib/gitlab/ci/trace/section_parser_spec.rb87
-rw-r--r--spec/lib/gitlab/ci/trace_spec.rb87
-rw-r--r--spec/lib/gitlab/diff/formatters/image_formatter_spec.rb20
-rw-r--r--spec/lib/gitlab/diff/formatters/text_formatter_spec.rb42
-rw-r--r--spec/lib/gitlab/diff/position_spec.rb85
-rw-r--r--spec/lib/gitlab/diff/position_tracer_spec.rb16
-rw-r--r--spec/lib/gitlab/git_access_spec.rb23
-rw-r--r--spec/lib/gitlab/git_access_wiki_spec.rb29
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml4
-rw-r--r--spec/lib/gitlab/import_export/fork_spec.rb6
-rw-r--r--spec/lib/gitlab/import_export/merge_request_parser_spec.rb7
-rw-r--r--spec/lib/gitlab/middleware/read_only_spec.rb142
-rw-r--r--spec/lib/gitlab/search_results_spec.rb4
-rw-r--r--spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb8
-rw-r--r--spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb5
-rw-r--r--spec/models/ci/build_spec.rb12
-rw-r--r--spec/models/ci/build_trace_section_name_spec.rb12
-rw-r--r--spec/models/ci/build_trace_section_spec.rb11
-rw-r--r--spec/models/concerns/cache_markdown_field_spec.rb72
-rw-r--r--spec/models/concerns/routable_spec.rb10
-rw-r--r--spec/models/diff_note_spec.rb40
-rw-r--r--spec/models/fork_network_member_spec.rb8
-rw-r--r--spec/models/fork_network_spec.rb54
-rw-r--r--spec/models/forked_project_link_spec.rb12
-rw-r--r--spec/models/merge_request_spec.rb54
-rw-r--r--spec/models/namespace_spec.rb23
-rw-r--r--spec/models/note_spec.rb50
-rw-r--r--spec/models/project_spec.rb96
-rw-r--r--spec/models/user_spec.rb65
-rw-r--r--spec/requests/api/merge_requests_spec.rb40
-rw-r--r--spec/requests/api/projects_spec.rb5
-rw-r--r--spec/requests/api/v3/merge_requests_spec.rb40
-rw-r--r--spec/requests/lfs_http_spec.rb34
-rw-r--r--spec/serializers/build_details_entity_spec.rb10
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb8
-rw-r--r--spec/services/ci/extract_sections_from_build_trace_service_spec.rb55
-rw-r--r--spec/services/ci/retry_build_service_spec.rb2
-rw-r--r--spec/services/delete_merged_branches_service_spec.rb6
-rw-r--r--spec/services/discussions/update_diff_position_service_spec.rb6
-rw-r--r--spec/services/merge_requests/conflicts/resolve_service_spec.rb20
-rw-r--r--spec/services/merge_requests/get_urls_service_spec.rb4
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb8
-rw-r--r--spec/services/projects/destroy_service_spec.rb13
-rw-r--r--spec/services/projects/fork_service_spec.rb39
-rw-r--r--spec/services/projects/hashed_storage_migration_service_spec.rb2
-rw-r--r--spec/services/projects/unlink_fork_service_spec.rb30
-rw-r--r--spec/services/projects/update_service_spec.rb10
-rw-r--r--spec/services/users/activity_service_spec.rb12
-rw-r--r--spec/support/features/discussion_comments_shared_example.rb2
-rw-r--r--spec/support/project_forks_helper.rb58
-rw-r--r--spec/support/shared_examples/position_formatters.rb43
-rw-r--r--spec/support/test_env.rb3
-rw-r--r--spec/views/projects/merge_requests/_commits.html.haml_spec.rb5
-rw-r--r--spec/views/projects/merge_requests/edit.html.haml_spec.rb9
-rw-r--r--spec/views/projects/merge_requests/show.html.haml_spec.rb11
-rw-r--r--spec/workers/build_finished_worker_spec.rb2
-rw-r--r--spec/workers/build_trace_sections_worker_spec.rb23
-rw-r--r--spec/workers/namespaceless_project_destroy_worker_spec.rb10
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 @@
+Running with gitlab-runner dev (HEAD)
+ on kitsune minikube (a21b584f)
+WARNING: Namespace is empty, therefore assuming 'default'.
+Using Kubernetes namespace: default
+Using Kubernetes executor with image alpine:3.4 ...
+section_start:1506004954:prepare_script Waiting 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 section_start:1506004957:get_sources Cloning repository...
+Cloning into '/nolith/ci-tests'...
+Checking out dddd7a6e as master...
+Skipping Git submodules setup
+section_end:1506004958:get_sources section_start:1506004958:restore_cache section_end:1506004958:restore_cache section_start:1506004958:download_artifacts section_end:1506004958:download_artifacts section_start:1506004958:build_script $ whoami
+root
+section_end:1506004959:build_script section_start:1506004959:after_script section_end:1506004959:after_script section_start:1506004959:archive_cache section_end:1506004959:archive_cache section_start:1506004959:upload_artifacts section_end:1506004959:upload_artifacts Job succeeded
+
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