diff options
Diffstat (limited to 'spec')
190 files changed, 5052 insertions, 1231 deletions
diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb index 07bf8d2d1c3..1d3c9fbbe2f 100644 --- a/spec/controllers/import/bitbucket_controller_spec.rb +++ b/spec/controllers/import/bitbucket_controller_spec.rb @@ -146,21 +146,42 @@ describe Import::BitbucketController do end context "when a namespace with the Bitbucket user's username doesn't exist" do - it "creates the namespace" do - expect(Gitlab::BitbucketImport::ProjectCreator). - to receive(:new).and_return(double(execute: true)) + context "when current user can create namespaces" do + it "creates the namespace" do + expect(Gitlab::BitbucketImport::ProjectCreator). + to receive(:new).and_return(double(execute: true)) - post :create, format: :js + expect { post :create, format: :js }.to change(Namespace, :count).by(1) + end + + it "takes the new namespace" do + expect(Gitlab::BitbucketImport::ProjectCreator). + to receive(:new).with(bitbucket_repo, an_instance_of(Group), user, access_params). + and_return(double(execute: true)) - expect(Namespace.where(name: other_username).first).not_to be_nil + post :create, format: :js + end end - it "takes the new namespace" do - expect(Gitlab::BitbucketImport::ProjectCreator). - to receive(:new).with(bitbucket_repo, an_instance_of(Group), user, access_params). - and_return(double(execute: true)) + context "when current user can't create namespaces" do + before do + user.update_attribute(:can_create_group, false) + end - post :create, format: :js + it "doesn't create the namespace" do + expect(Gitlab::BitbucketImport::ProjectCreator). + to receive(:new).and_return(double(execute: true)) + + expect { post :create, format: :js }.not_to change(Namespace, :count) + end + + it "takes the current user's namespace" do + expect(Gitlab::BitbucketImport::ProjectCreator). + to receive(:new).with(bitbucket_repo, user.namespace, user, access_params). + and_return(double(execute: true)) + + post :create, format: :js + end end end end diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb index 51d59526854..ebfbf54182b 100644 --- a/spec/controllers/import/github_controller_spec.rb +++ b/spec/controllers/import/github_controller_spec.rb @@ -181,21 +181,42 @@ describe Import::GithubController do end context "when a namespace with the GitHub user's username doesn't exist" do - it "creates the namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).and_return(double(execute: true)) + context "when current user can create namespaces" do + it "creates the namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).and_return(double(execute: true)) - post :create, format: :js + expect { post :create, format: :js }.to change(Namespace, :count).by(1) + end + + it "takes the new namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(github_repo, an_instance_of(Group), user, access_params). + and_return(double(execute: true)) - expect(Namespace.where(name: other_username).first).not_to be_nil + post :create, format: :js + end end - it "takes the new namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(github_repo, an_instance_of(Group), user, access_params). - and_return(double(execute: true)) + context "when current user can't create namespaces" do + before do + user.update_attribute(:can_create_group, false) + end - post :create, format: :js + it "doesn't create the namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).and_return(double(execute: true)) + + expect { post :create, format: :js }.not_to change(Namespace, :count) + end + + it "takes the current user's namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(github_repo, user.namespace, user, access_params). + and_return(double(execute: true)) + + post :create, format: :js + end end end end diff --git a/spec/controllers/import/gitlab_controller_spec.rb b/spec/controllers/import/gitlab_controller_spec.rb index e8cf6aa7767..6f75ebb16c8 100644 --- a/spec/controllers/import/gitlab_controller_spec.rb +++ b/spec/controllers/import/gitlab_controller_spec.rb @@ -136,21 +136,42 @@ describe Import::GitlabController do end context "when a namespace with the GitLab.com user's username doesn't exist" do - it "creates the namespace" do - expect(Gitlab::GitlabImport::ProjectCreator). - to receive(:new).and_return(double(execute: true)) + context "when current user can create namespaces" do + it "creates the namespace" do + expect(Gitlab::GitlabImport::ProjectCreator). + to receive(:new).and_return(double(execute: true)) - post :create, format: :js + expect { post :create, format: :js }.to change(Namespace, :count).by(1) + end + + it "takes the new namespace" do + expect(Gitlab::GitlabImport::ProjectCreator). + to receive(:new).with(gitlab_repo, an_instance_of(Group), user, access_params). + and_return(double(execute: true)) - expect(Namespace.where(name: other_username).first).not_to be_nil + post :create, format: :js + end end - it "takes the new namespace" do - expect(Gitlab::GitlabImport::ProjectCreator). - to receive(:new).with(gitlab_repo, an_instance_of(Group), user, access_params). - and_return(double(execute: true)) + context "when current user can't create namespaces" do + before do + user.update_attribute(:can_create_group, false) + end - post :create, format: :js + it "doesn't create the namespace" do + expect(Gitlab::GitlabImport::ProjectCreator). + to receive(:new).and_return(double(execute: true)) + + expect { post :create, format: :js }.not_to change(Namespace, :count) + end + + it "takes the current user's namespace" do + expect(Gitlab::GitlabImport::ProjectCreator). + to receive(:new).with(gitlab_repo, user.namespace, user, access_params). + and_return(double(execute: true)) + + post :create, format: :js + end end end end diff --git a/spec/controllers/import/gitorious_controller_spec.rb b/spec/controllers/import/gitorious_controller_spec.rb deleted file mode 100644 index 4ae2b78e11c..00000000000 --- a/spec/controllers/import/gitorious_controller_spec.rb +++ /dev/null @@ -1,69 +0,0 @@ -require 'spec_helper' - -describe Import::GitoriousController do - include ImportSpecHelper - - let(:user) { create(:user) } - - before do - sign_in(user) - end - - describe "GET new" do - it "redirects to import endpoint on gitorious.org" do - get :new - - expect(controller).to redirect_to("https://gitorious.org/gitlab-import?callback_url=http://test.host/import/gitorious/callback") - end - end - - describe "GET callback" do - it "stores repo list in session" do - get :callback, repos: 'foo/bar,baz/qux' - - expect(session[:gitorious_repos]).to eq('foo/bar,baz/qux') - end - end - - describe "GET status" do - before do - @repo = OpenStruct.new(full_name: 'asd/vim') - end - - it "assigns variables" do - @project = create(:project, import_type: 'gitorious', creator_id: user.id) - stub_client(repos: [@repo]) - - get :status - - expect(assigns(:already_added_projects)).to eq([@project]) - expect(assigns(:repos)).to eq([@repo]) - end - - it "does not show already added project" do - @project = create(:project, import_type: 'gitorious', creator_id: user.id, import_source: 'asd/vim') - stub_client(repos: [@repo]) - - get :status - - expect(assigns(:already_added_projects)).to eq([@project]) - expect(assigns(:repos)).to eq([]) - end - end - - describe "POST create" do - before do - @repo = Gitlab::GitoriousImport::Repository.new('asd/vim') - end - - it "takes already existing namespace" do - namespace = create(:namespace, name: "asd", owner: user) - expect(Gitlab::GitoriousImport::ProjectCreator). - to receive(:new).with(@repo, namespace, user). - and_return(double(execute: true)) - stub_client(repo: @repo) - - post :create, format: :js - end - end -end diff --git a/spec/controllers/projects/boards/issues_controller_spec.rb b/spec/controllers/projects/boards/issues_controller_spec.rb index d0ad5e26dbd..2896636db5a 100644 --- a/spec/controllers/projects/boards/issues_controller_spec.rb +++ b/spec/controllers/projects/boards/issues_controller_spec.rb @@ -41,8 +41,8 @@ describe Projects::Boards::IssuesController do context 'with unauthorized user' do before do - allow(Ability.abilities).to receive(:allowed?).with(user, :read_project, project).and_return(true) - allow(Ability.abilities).to receive(:allowed?).with(user, :read_issue, project).and_return(false) + allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true) + allow(Ability).to receive(:allowed?).with(user, :read_issue, project).and_return(false) end it 'returns a successful 403 response' do diff --git a/spec/controllers/projects/boards/lists_controller_spec.rb b/spec/controllers/projects/boards/lists_controller_spec.rb index 9496636e3cc..d687dea3c3b 100644 --- a/spec/controllers/projects/boards/lists_controller_spec.rb +++ b/spec/controllers/projects/boards/lists_controller_spec.rb @@ -35,11 +35,11 @@ describe Projects::Boards::ListsController do context 'with unauthorized user' do before do - allow(Ability.abilities).to receive(:allowed?).with(user, :read_project, project).and_return(true) - allow(Ability.abilities).to receive(:allowed?).with(user, :read_list, project).and_return(false) + allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true) + allow(Ability).to receive(:allowed?).with(user, :read_list, project).and_return(false) end - it 'returns a successful 403 response' do + it 'returns a forbidden 403 response' do read_board_list user: user expect(response).to have_http_status(403) @@ -56,9 +56,9 @@ describe Projects::Boards::ListsController do end describe 'POST create' do - let(:label) { create(:label, project: project, name: 'Development') } - context 'with valid params' do + let(:label) { create(:label, project: project, name: 'Development') } + it 'returns a successful 200 response' do create_board_list user: user, label_id: label.id @@ -73,20 +73,29 @@ describe Projects::Boards::ListsController do end context 'with invalid params' do - it 'returns an error' do - create_board_list user: user, label_id: nil + context 'when label is nil' do + it 'returns a not found 404 response' do + create_board_list user: user, label_id: nil + + expect(response).to have_http_status(404) + end + end - parsed_response = JSON.parse(response.body) + context 'when label that does not belongs to project' do + it 'returns a not found 404 response' do + label = create(:label, name: 'Development') - expect(parsed_response['label']).to contain_exactly "can't be blank" - expect(response).to have_http_status(422) + create_board_list user: user, label_id: label.id + + expect(response).to have_http_status(404) + end end end context 'with unauthorized user' do - let(:label) { create(:label, project: project, name: 'Development') } + it 'returns a forbidden 403 response' do + label = create(:label, project: project, name: 'Development') - it 'returns a successful 403 response' do create_board_list user: guest, label_id: label.id expect(response).to have_http_status(403) @@ -122,7 +131,7 @@ describe Projects::Boards::ListsController do end context 'with invalid position' do - it 'returns a unprocessable entity 422 response' do + it 'returns an unprocessable entity 422 response' do move user: user, list: planning, position: 6 expect(response).to have_http_status(422) @@ -138,7 +147,7 @@ describe Projects::Boards::ListsController do end context 'with unauthorized user' do - it 'returns a successful 403 response' do + it 'returns a forbidden 403 response' do move user: guest, list: planning, position: 6 expect(response).to have_http_status(403) @@ -180,7 +189,7 @@ describe Projects::Boards::ListsController do end context 'with unauthorized user' do - it 'returns a successful 403 response' do + it 'returns a forbidden 403 response' do remove_board_list user: guest, list: planning expect(response).to have_http_status(403) @@ -213,7 +222,7 @@ describe Projects::Boards::ListsController do end context 'when board lists is not empty' do - it 'returns a unprocessable entity 422 response' do + it 'returns an unprocessable entity 422 response' do create(:list, board: board) generate_default_board_lists user: user @@ -223,7 +232,7 @@ describe Projects::Boards::ListsController do end context 'with unauthorized user' do - it 'returns a successful 403 response' do + it 'returns a forbidden 403 response' do generate_default_board_lists user: guest expect(response).to have_http_status(403) diff --git a/spec/controllers/projects/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb index 75a6d39e82c..6f6e608e1f3 100644 --- a/spec/controllers/projects/boards_controller_spec.rb +++ b/spec/controllers/projects/boards_controller_spec.rb @@ -23,8 +23,8 @@ describe Projects::BoardsController do context 'with unauthorized user' do before do - allow(Ability.abilities).to receive(:allowed?).with(user, :read_project, project).and_return(true) - allow(Ability.abilities).to receive(:allowed?).with(user, :read_board, project).and_return(false) + allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true) + allow(Ability).to receive(:allowed?).with(user, :read_board, project).and_return(false) end it 'returns a successful 404 response' do diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 0836b71056c..90419368f22 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -8,13 +8,13 @@ describe Projects::IssuesController do describe "GET #index" do context 'external issue tracker' do it 'redirects to the external issue tracker' do - external = double(issues_url: 'https://example.com/issues') + external = double(project_path: 'https://example.com/project') allow(project).to receive(:external_issue_tracker).and_return(external) controller.instance_variable_set(:@project, project) get :index, namespace_id: project.namespace.path, project_id: project - expect(response).to redirect_to('https://example.com/issues') + expect(response).to redirect_to('https://example.com/project') end end @@ -370,6 +370,12 @@ describe Projects::IssuesController do expect(response).to have_http_status(302) expect(controller).to set_flash[:notice].to(/The issue was successfully deleted\./).now end + + it 'delegates the update of the todos count cache to TodoService' do + expect_any_instance_of(TodoService).to receive(:destroy_issue).with(issue, owner).once + + delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: issue.iid + end end end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index c64c2b075c5..94c9edc91fe 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -170,6 +170,35 @@ describe Projects::MergeRequestsController do expect(response).to redirect_to([merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request]) expect(merge_request.reload.closed?).to be_truthy end + + it 'allows editing of a closed merge request' do + merge_request.close! + + put :update, + namespace_id: project.namespace.path, + project_id: project.path, + id: merge_request.iid, + merge_request: { + title: 'New title' + } + + expect(response).to redirect_to([merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request]) + expect(merge_request.reload.title).to eq 'New title' + end + + it 'does not allow to update target branch closed merge request' do + merge_request.close! + + put :update, + namespace_id: project.namespace.path, + project_id: project.path, + id: merge_request.iid, + merge_request: { + target_branch: 'new_branch' + } + + expect { merge_request.reload.target_branch }.not_to change { merge_request.target_branch } + end end end @@ -291,6 +320,12 @@ describe Projects::MergeRequestsController do expect(response).to have_http_status(302) expect(controller).to set_flash[:notice].to(/The merge request was successfully deleted\./).now end + + it 'delegates the update of the todos count cache to TodoService' do + expect_any_instance_of(TodoService).to receive(:destroy_merge_request).with(merge_request, owner).once + + delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid + end end end diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb index cccd492ef06..2e44b5128b4 100644 --- a/spec/controllers/projects/services_controller_spec.rb +++ b/spec/controllers/projects/services_controller_spec.rb @@ -49,4 +49,20 @@ describe Projects::ServicesController do let!(:referrer) { nil } end end + + describe 'PUT #update' do + context 'on successful update' do + it 'sets the flash' do + expect(service).to receive(:to_param).and_return('hipchat') + + put :update, + namespace_id: project.namespace.id, + project_id: project.id, + id: service.id, + service: { active: false } + + expect(flash[:notice]).to eq 'Successfully updated.' + end + end + end end diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb index b8a28f43707..72a3ebf2ebd 100644 --- a/spec/controllers/projects/snippets_controller_spec.rb +++ b/spec/controllers/projects/snippets_controller_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Projects::SnippetsController do - let(:project) { create(:project_empty_repo, :public, snippets_enabled: true) } + let(:project) { create(:project_empty_repo, :public) } let(:user) { create(:user) } let(:user2) { create(:user) } diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index 4e9bfb0c69b..8f27e616c3e 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -136,6 +136,29 @@ describe SessionsController do post(:create, { user: user_params }, { otp_user_id: user.id }) end + context 'remember_me field' do + it 'sets a remember_user_token cookie when enabled' do + allow(U2fRegistration).to receive(:authenticate).and_return(true) + allow(controller).to receive(:find_user).and_return(user) + expect(controller). + to receive(:remember_me).with(user).and_call_original + + authenticate_2fa_u2f(remember_me: '1', login: user.username, device_response: "{}") + + expect(response.cookies['remember_user_token']).to be_present + end + + it 'does nothing when disabled' do + allow(U2fRegistration).to receive(:authenticate).and_return(true) + allow(controller).to receive(:find_user).and_return(user) + expect(controller).not_to receive(:remember_me) + + authenticate_2fa_u2f(remember_me: '0', login: user.username, device_response: "{}") + + expect(response.cookies['remember_user_token']).to be_nil + end + end + it "creates an audit log record" do allow(U2fRegistration).to receive(:authenticate).and_return(true) expect { authenticate_2fa_u2f(login: user.username, device_response: "{}") }.to change { SecurityEvent.count }.by(1) diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index 83e38095feb..6919002dedc 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -28,6 +28,11 @@ FactoryGirl.define do diff_refs: noteable.diff_refs ) end + + trait :resolved do + resolved_at { Time.now } + resolved_by { create(:user) } + end end factory :diff_note_on_commit, traits: [:on_commit], class: DiffNote do diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index f82d68a1816..fb84ba07d25 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -8,7 +8,6 @@ FactoryGirl.define do path { name.downcase.gsub(/\s/, '_') } namespace creator - snippets_enabled true trait :public do visibility_level Gitlab::VisibilityLevel::PUBLIC @@ -27,6 +26,26 @@ FactoryGirl.define do project.create_repository end end + + # Nest Project Feature attributes + transient do + wiki_access_level ProjectFeature::ENABLED + builds_access_level ProjectFeature::ENABLED + snippets_access_level ProjectFeature::ENABLED + issues_access_level ProjectFeature::ENABLED + merge_requests_access_level ProjectFeature::ENABLED + end + + after(:create) do |project, evaluator| + project.project_feature. + update_attributes( + wiki_access_level: evaluator.wiki_access_level, + builds_access_level: evaluator.builds_access_level, + snippets_access_level: evaluator.snippets_access_level, + issues_access_level: evaluator.issues_access_level, + merge_requests_access_level: evaluator.merge_requests_access_level, + ) + end end # Project with empty repository diff --git a/spec/features/admin/admin_system_info_spec.rb b/spec/features/admin/admin_system_info_spec.rb index f4e5c26b519..1df972843e2 100644 --- a/spec/features/admin/admin_system_info_spec.rb +++ b/spec/features/admin/admin_system_info_spec.rb @@ -6,12 +6,49 @@ describe 'Admin System Info' do end describe 'GET /admin/system_info' do - it 'shows system info page' do - visit admin_system_info_path + let(:cpu) { double(:cpu, length: 2) } + let(:memory) { double(:memory, active_bytes: 4294967296, total_bytes: 17179869184) } - expect(page).to have_content 'CPU' - expect(page).to have_content 'Memory' - expect(page).to have_content 'Disks' + context 'when all info is available' do + before do + allow(Vmstat).to receive(:cpu).and_return(cpu) + allow(Vmstat).to receive(:memory).and_return(memory) + visit admin_system_info_path + end + + it 'shows system info page' do + expect(page).to have_content 'CPU 2 cores' + expect(page).to have_content 'Memory 4 GB / 16 GB' + expect(page).to have_content 'Disks' + end + end + + context 'when CPU info is not available' do + before do + allow(Vmstat).to receive(:cpu).and_raise(Errno::ENOENT) + allow(Vmstat).to receive(:memory).and_return(memory) + visit admin_system_info_path + end + + it 'shows system info page with no CPU info' do + expect(page).to have_content 'CPU Unable to collect CPU info' + expect(page).to have_content 'Memory 4 GB / 16 GB' + expect(page).to have_content 'Disks' + end + end + + context 'when memory info is not available' do + before do + allow(Vmstat).to receive(:cpu).and_return(cpu) + allow(Vmstat).to receive(:memory).and_raise(Errno::ENOENT) + visit admin_system_info_path + end + + it 'shows system info page with no CPU info' do + expect(page).to have_content 'CPU 2 cores' + expect(page).to have_content 'Memory Unable to collect memory info' + expect(page).to have_content 'Disks' + end end end end diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index 5d777895542..c6c2e2095df 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -110,6 +110,45 @@ describe 'Issue Boards', feature: true, js: true do end end + it 'search backlog list' do + page.within('#js-boards-seach') do + find('.form-control').set(issue1.title) + end + + wait_for_vue_resource + + expect(find('.board:nth-child(1)')).to have_selector('.card', count: 1) + expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0) + expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0) + expect(find('.board:nth-child(4)')).to have_selector('.card', count: 0) + end + + it 'search done list' do + page.within('#js-boards-seach') do + find('.form-control').set(issue8.title) + end + + wait_for_vue_resource + + expect(find('.board:nth-child(1)')).to have_selector('.card', count: 0) + expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0) + expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0) + expect(find('.board:nth-child(4)')).to have_selector('.card', count: 1) + end + + it 'search list' do + page.within('#js-boards-seach') do + find('.form-control').set(issue5.title) + end + + wait_for_vue_resource + + expect(find('.board:nth-child(1)')).to have_selector('.card', count: 0) + expect(find('.board:nth-child(2)')).to have_selector('.card', count: 1) + expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0) + expect(find('.board:nth-child(4)')).to have_selector('.card', count: 0) + end + it 'allows user to delete board' do page.within(find('.board:nth-child(2)')) do find('.board-delete').click @@ -143,14 +182,21 @@ describe 'Issue Boards', feature: true, js: true do wait_for_vue_resource page.within(find('.board', match: :first)) do - expect(page.find('.board-header')).to have_content('20') + expect(page.find('.board-header')).to have_content('56') expect(page).to have_selector('.card', count: 20) + expect(page).to have_content('Showing 20 of 56 issues') evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight") wait_for_vue_resource(spinner: false) - expect(page.find('.board-header')).to have_content('40') expect(page).to have_selector('.card', count: 40) + expect(page).to have_content('Showing 40 of 56 issues') + + evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight") + wait_for_vue_resource(spinner: false) + + expect(page).to have_selector('.card', count: 56) + expect(page).to have_content('Showing all issues') end end @@ -162,32 +208,6 @@ describe 'Issue Boards', feature: true, js: true do end end - it 'is searchable' do - page.within(find('.board', match: :first)) do - find('.form-control').set issue1.title - - wait_for_vue_resource(spinner: false) - - expect(page).to have_selector('.card', count: 1) - end - end - - it 'clears search' do - page.within(find('.board', match: :first)) do - find('.form-control').set issue1.title - - expect(page).to have_selector('.card', count: 1) - - find('.board-search-clear-btn').click - end - - wait_for_vue_resource - - page.within(find('.board', match: :first)) do - expect(page).to have_selector('.card', count: 6) - end - end - it 'moves issue from backlog into list' do drag_to(list_to_index: 1) @@ -466,13 +486,19 @@ describe 'Issue Boards', feature: true, js: true do wait_for_vue_resource page.within(find('.board', match: :first)) do - expect(page.find('.board-header')).to have_content('20') + expect(page.find('.board-header')).to have_content('51') expect(page).to have_selector('.card', count: 20) + expect(page).to have_content('Showing 20 of 51 issues') evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight") - expect(page.find('.board-header')).to have_content('40') expect(page).to have_selector('.card', count: 40) + expect(page).to have_content('Showing 40 of 51 issues') + + evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight") + + expect(page).to have_selector('.card', count: 51) + expect(page).to have_content('Showing all issues') end end diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb index 688f68d3cff..8863554ee91 100644 --- a/spec/features/expand_collapse_diffs_spec.rb +++ b/spec/features/expand_collapse_diffs_spec.rb @@ -211,6 +211,13 @@ feature 'Expand and collapse diffs', js: true, feature: true do context 'expanding all diffs' do before do click_link('Expand all') + + # Wait for elements to appear to ensure full page reload + expect(page).to have_content('This diff was suppressed by a .gitattributes entry') + expect(page).to have_content('This diff could not be displayed because it is too large.') + expect(page).to have_content('too_large_image.jpg') + find('.note-textarea') + wait_for_ajax execute_script('window.ajaxUris = []; $(document).ajaxSend(function(event, xhr, settings) { ajaxUris.push(settings.url) });') end diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb index 6eb04cf74c5..79cc50bc18e 100644 --- a/spec/features/issues/award_emoji_spec.rb +++ b/spec/features/issues/award_emoji_spec.rb @@ -12,7 +12,6 @@ describe 'Awards Emoji', feature: true do describe 'Click award emoji from issue#show' do let!(:issue) do create(:issue, - author: @user, assignee: @user, project: project) end diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index e262f285868..0e9f814044e 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -8,6 +8,7 @@ describe 'Filter issues', feature: true do let!(:milestone) { create(:milestone, project: project) } let!(:label) { create(:label, project: project) } let!(:issue1) { create(:issue, project: project) } + let!(:wontfix) { create(:label, project: project, title: "Won't fix") } before do project.team << [user, :master] @@ -107,6 +108,15 @@ describe 'Filter issues', feature: true do end expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title) end + + it 'filters by wont fix labels' do + find('.dropdown-menu-labels a', text: label.title).click + page.within '.labels-filter' do + expect(page).to have_content wontfix.title + click_link wontfix.title + end + expect(find('.js-label-select .dropdown-toggle-text')).to have_content(wontfix.title) + end end describe 'Filter issues for assignee and label from issues#index' do diff --git a/spec/features/issues/new_branch_button_spec.rb b/spec/features/issues/new_branch_button_spec.rb index e528aff4d41..fb0c4704285 100644 --- a/spec/features/issues/new_branch_button_spec.rb +++ b/spec/features/issues/new_branch_button_spec.rb @@ -20,7 +20,7 @@ feature 'Start new branch from an issue', feature: true do context "when there is a referenced merge request" do let(:note) do create(:note, :on_issue, :system, project: project, - note: "mentioned in !#{referenced_mr.iid}") + note: "Mentioned in !#{referenced_mr.iid}") end let(:referenced_mr) do create(:merge_request, :simple, source_project: project, target_project: project, diff --git a/spec/features/issues/reset_filters_spec.rb b/spec/features/issues/reset_filters_spec.rb new file mode 100644 index 00000000000..41f218eaa8b --- /dev/null +++ b/spec/features/issues/reset_filters_spec.rb @@ -0,0 +1,81 @@ +require 'rails_helper' + +feature 'Issues filter reset button', feature: true, js: true do + include WaitForAjax + include IssueHelpers + + let!(:project) { create(:project, :public) } + let!(:user) { create(:user)} + let!(:milestone) { create(:milestone, project: project) } + let!(:bug) { create(:label, project: project, name: 'bug')} + let!(:issue1) { create(:issue, project: project, milestone: milestone, author: user, assignee: user, title: 'Feature')} + let!(:issue2) { create(:labeled_issue, project: project, labels: [bug], title: 'Bugfix1')} + + before do + project.team << [user, :developer] + end + + context 'when a milestone filter has been applied' do + it 'resets the milestone filter' do + visit_issues(project, milestone_title: milestone.title) + expect(page).to have_css('.issue', count: 1) + + reset_filters + expect(page).to have_css('.issue', count: 2) + end + end + + context 'when a label filter has been applied' do + it 'resets the label filter' do + visit_issues(project, label_name: bug.name) + expect(page).to have_css('.issue', count: 1) + + reset_filters + expect(page).to have_css('.issue', count: 2) + end + end + + context 'when a text search has been conducted' do + it 'resets the text search filter' do + visit_issues(project, issue_search: 'Bug') + expect(page).to have_css('.issue', count: 1) + + reset_filters + expect(page).to have_css('.issue', count: 2) + end + end + + context 'when author filter has been applied' do + it 'resets the author filter' do + visit_issues(project, author_id: user.id) + expect(page).to have_css('.issue', count: 1) + + reset_filters + expect(page).to have_css('.issue', count: 2) + end + end + + context 'when assignee filter has been applied' do + it 'resets the assignee filter' do + visit_issues(project, assignee_id: user.id) + expect(page).to have_css('.issue', count: 1) + + reset_filters + expect(page).to have_css('.issue', count: 2) + end + end + + context 'when all filters have been applied' do + it 'resets all filters' do + visit_issues(project, assignee_id: user.id, author_id: user.id, milestone_title: milestone.title, label_name: bug.name, issue_search: 'Bug') + expect(page).to have_css('.issue', count: 0) + + reset_filters + expect(page).to have_css('.issue', count: 2) + end + end + + def reset_filters + find('.reset-filters').click + end +end diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb index 2883e392694..105629c485a 100644 --- a/spec/features/issues/user_uses_slash_commands_spec.rb +++ b/spec/features/issues/user_uses_slash_commands_spec.rb @@ -1,6 +1,7 @@ require 'rails_helper' feature 'Issues > User uses slash commands', feature: true, js: true do + include SlashCommandsHelpers include WaitForAjax it_behaves_like 'issuable record that supports slash commands in its description and notes', :issue do @@ -17,14 +18,15 @@ feature 'Issues > User uses slash commands', feature: true, js: true do visit namespace_project_issue_path(project.namespace, project, issue) end + after do + wait_for_ajax + end + describe 'adding a due date from note' do let(:issue) { create(:issue, project: project) } it 'does not create a note, and sets the due date accordingly' do - page.within('.js-main-target-form') do - fill_in 'note[note]', with: "/due 2016-08-28" - click_button 'Comment' - end + write_note("/due 2016-08-28") expect(page).not_to have_content '/due 2016-08-28' expect(page).to have_content 'Your commands have been executed!' @@ -41,10 +43,7 @@ feature 'Issues > User uses slash commands', feature: true, js: true do it 'does not create a note, and removes the due date accordingly' do expect(issue.due_date).to eq Date.new(2016, 8, 28) - page.within('.js-main-target-form') do - fill_in 'note[note]', with: "/remove_due_date" - click_button 'Comment' - end + write_note("/remove_due_date") expect(page).not_to have_content '/remove_due_date' expect(page).to have_content 'Your commands have been executed!' diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb index 930c36ade2b..759edf8ec80 100644 --- a/spec/features/merge_requests/conflicts_spec.rb +++ b/spec/features/merge_requests/conflicts_spec.rb @@ -43,7 +43,8 @@ feature 'Merge request conflict resolution', js: true, feature: true do 'conflict-too-large' => 'when the conflicts contain a large file', 'conflict-binary-file' => 'when the conflicts contain a binary file', 'conflict-contains-conflict-markers' => 'when the conflicts contain a file with ambiguous conflict markers', - 'conflict-missing-side' => 'when the conflicts contain a file edited in one branch and deleted in another' + 'conflict-missing-side' => 'when the conflicts contain a file edited in one branch and deleted in another', + 'conflict-non-utf8' => 'when the conflicts contain a non-UTF-8 file', } UNRESOLVABLE_CONFLICTS.each do |source_branch, description| diff --git a/spec/features/merge_requests/diff_notes_spec.rb b/spec/features/merge_requests/diff_notes_spec.rb index a818679a874..06fad1007e8 100644 --- a/spec/features/merge_requests/diff_notes_spec.rb +++ b/spec/features/merge_requests/diff_notes_spec.rb @@ -147,6 +147,37 @@ feature 'Diff notes', js: true, feature: true do end end + context 'when the MR only supports legacy diff notes' do + before do + @merge_request.merge_request_diff.update_attributes(start_commit_sha: nil) + visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, view: 'inline') + end + + context 'with a new line' do + it 'should allow commenting' do + should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]')) + end + end + + context 'with an old line' do + it 'should allow commenting' do + should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]')) + end + end + + context 'with an unchanged line' do + it 'should allow commenting' do + should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]')) + end + end + + context 'with a match line' do + it 'should not allow commenting' do + should_not_allow_commenting(find('.match', match: :first)) + end + end + end + def should_allow_commenting(line_holder, diff_side = nil) line = get_line_components(line_holder, diff_side) line[:content].hover diff --git a/spec/features/merge_requests/merge_request_versions_spec.rb b/spec/features/merge_requests/merge_request_versions_spec.rb new file mode 100644 index 00000000000..9e759de3752 --- /dev/null +++ b/spec/features/merge_requests/merge_request_versions_spec.rb @@ -0,0 +1,68 @@ +require 'spec_helper' + +feature 'Merge Request versions', js: true, feature: true do + before do + login_as :admin + merge_request = create(:merge_request, importing: true) + merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') + merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') + project = merge_request.source_project + visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'show the latest version of the diff' do + page.within '.mr-version-dropdown' do + expect(page).to have_content 'latest version' + end + + expect(page).to have_content '8 changed files' + end + + describe 'switch between versions' do + before do + page.within '.mr-version-dropdown' do + find('.btn-link').click + click_link 'version 1' + end + end + + it 'should show older version' do + page.within '.mr-version-dropdown' do + expect(page).to have_content 'version 1' + end + + expect(page).to have_content '5 changed files' + end + + it 'show the message about disabled comments' do + expect(page).to have_content 'Comments are disabled' + end + end + + describe 'compare with older version' do + before do + page.within '.mr-version-compare-dropdown' do + find('.btn-link').click + click_link 'version 1' + end + end + + it 'should has correct value in the compare dropdown' do + page.within '.mr-version-compare-dropdown' do + expect(page).to have_content 'version 1' + end + end + + it 'show the message about disabled comments' do + expect(page).to have_content 'Comments are disabled' + end + + it 'show diff between new and old version' do + expect(page).to have_content '4 changed files with 15 additions and 6 deletions' + end + + it 'show diff between new and old version' do + expect(page).to have_content '4 changed files with 15 additions and 6 deletions' + end + end +end diff --git a/spec/features/merge_requests/update_merge_requests_spec.rb b/spec/features/merge_requests/update_merge_requests_spec.rb new file mode 100644 index 00000000000..b56fdfe5611 --- /dev/null +++ b/spec/features/merge_requests/update_merge_requests_spec.rb @@ -0,0 +1,132 @@ +require 'rails_helper' + +feature 'Multiple merge requests updating from merge_requests#index', feature: true do + include WaitForAjax + + let!(:user) { create(:user)} + let!(:project) { create(:project) } + let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) } + + before do + project.team << [user, :master] + login_as(user) + end + + context 'status', js: true do + describe 'close merge request' do + before do + visit namespace_project_merge_requests_path(project.namespace, project) + end + + it 'closes merge request' do + change_status('Closed') + + expect(page).to have_selector('.merge-request', count: 0) + end + end + + describe 'reopen merge request' do + before do + merge_request.close + visit namespace_project_merge_requests_path(project.namespace, project, state: 'closed') + end + + it 'reopens merge request' do + change_status('Open') + + expect(page).to have_selector('.merge-request', count: 0) + end + end + end + + context 'assignee', js: true do + describe 'set assignee' do + before do + visit namespace_project_merge_requests_path(project.namespace, project) + end + + it "updates merge request with assignee" do + change_assignee(user.name) + + page.within('.merge-request .controls') do + expect(find('.author_link')["title"]).to have_content(user.name) + end + end + end + + describe 'remove assignee' do + before do + merge_request.assignee = user + merge_request.save + visit namespace_project_merge_requests_path(project.namespace, project) + end + + it "removes assignee from the merge request" do + change_assignee('Unassigned') + + expect(find('.merge-request .controls')).not_to have_css('.author_link') + end + end + end + + context 'milestone', js: true do + let(:milestone) { create(:milestone, project: project) } + + describe 'set milestone' do + before do + visit namespace_project_merge_requests_path(project.namespace, project) + end + + it "updates merge request with milestone" do + change_milestone(milestone.title) + + expect(find('.merge-request')).to have_content milestone.title + end + end + + describe 'unset milestone' do + before do + merge_request.milestone = milestone + merge_request.save + visit namespace_project_merge_requests_path(project.namespace, project) + end + + it "removes milestone from the merge request" do + change_milestone("No Milestone") + + expect(find('.merge-request')).not_to have_content milestone.title + end + end + end + + def change_status(text) + find('#check_all_issues').click + find('.js-issue-status').click + find('.dropdown-menu-status a', text: text).click + click_update_merge_requests_button + end + + def change_assignee(text) + find('#check_all_issues').click + find('.js-update-assignee').click + wait_for_ajax + + page.within '.dropdown-menu-user' do + click_link text + end + + click_update_merge_requests_button + end + + def change_milestone(text) + find('#check_all_issues').click + find('.issues_bulk_update .js-milestone-select').click + find('.dropdown-menu-milestone a', text: text).click + click_update_merge_requests_button + end + + def click_update_merge_requests_button + find('.update_selected_issues').click + wait_for_ajax + end +end diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb index d9ef0d18074..22d9d1b9fd5 100644 --- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb +++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb @@ -1,6 +1,7 @@ require 'rails_helper' feature 'Merge Requests > User uses slash commands', feature: true, js: true do + include SlashCommandsHelpers include WaitForAjax let(:user) { create(:user) } @@ -20,11 +21,12 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do visit namespace_project_merge_request_path(project.namespace, project, merge_request) end + after do + wait_for_ajax + end + it 'does not recognize the command nor create a note' do - page.within('.js-main-target-form') do - fill_in 'note[note]', with: "/due 2016-08-28" - click_button 'Comment' - end + write_note("/due 2016-08-28") expect(page).not_to have_content '/due 2016-08-28' end diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index 7a9edbbe339..f1c522155d3 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -141,7 +141,7 @@ describe 'Comments', feature: true do let(:project2) { create(:project, :private) } let(:issue) { create(:issue, project: project2) } let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'markdown') } - let!(:note) { create(:note_on_merge_request, :system, noteable: merge_request, project: project, note: "mentioned in #{issue.to_reference(project)}") } + let!(:note) { create(:note_on_merge_request, :system, noteable: merge_request, project: project, note: "Mentioned in #{issue.to_reference(project)}") } it 'shows the system note' do login_as :admin diff --git a/spec/features/projects/branches/download_buttons_spec.rb b/spec/features/projects/branches/download_buttons_spec.rb new file mode 100644 index 00000000000..04058300570 --- /dev/null +++ b/spec/features/projects/branches/download_buttons_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +feature 'Download buttons in branches page', feature: true do + given(:user) { create(:user) } + given(:role) { :developer } + given(:status) { 'success' } + given(:project) { create(:project) } + + given(:pipeline) do + create(:ci_pipeline, + project: project, + sha: project.commit('binary-encoding').sha, + ref: 'binary-encoding', # make sure the branch is in the 1st page! + status: status) + end + + given!(:build) do + create(:ci_build, :success, :artifacts, + pipeline: pipeline, + status: pipeline.status, + name: 'build') + end + + background do + login_as(user) + project.team << [user, role] + end + + describe 'when checking branches' do + context 'with artifacts' do + before do + visit namespace_project_branches_path(project.namespace, project) + end + + scenario 'shows download artifacts button' do + expect(page).to have_link "Download '#{build.name}'" + end + end + end +end diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb index 1b14945bf0a..d26a0caf036 100644 --- a/spec/features/projects/branches_spec.rb +++ b/spec/features/projects/branches_spec.rb @@ -1,32 +1,46 @@ require 'spec_helper' describe 'Branches', feature: true do - let(:project) { create(:project) } + let(:project) { create(:project, :public) } let(:repository) { project.repository } - before do - login_as :user - project.team << [@user, :developer] - end + context 'logged in' do + before do + login_as :user + project.team << [@user, :developer] + end - describe 'Initial branches page' do - it 'shows all the branches' do - visit namespace_project_branches_path(project.namespace, project) + describe 'Initial branches page' do + it 'shows all the branches' do + visit namespace_project_branches_path(project.namespace, project) - repository.branches { |branch| expect(page).to have_content("#{branch.name}") } - expect(page).to have_content("Protected branches can be managed in project settings") + repository.branches { |branch| expect(page).to have_content("#{branch.name}") } + expect(page).to have_content("Protected branches can be managed in project settings") + end + end + + describe 'Find branches' do + it 'shows filtered branches', js: true do + visit namespace_project_branches_path(project.namespace, project) + + fill_in 'branch-search', with: 'fix' + find('#branch-search').native.send_keys(:enter) + + expect(page).to have_content('fix') + expect(find('.all-branches')).to have_selector('li', count: 1) + end end end - describe 'Find branches' do - it 'shows filtered branches', js: true do + context 'logged out' do + before do visit namespace_project_branches_path(project.namespace, project) + end - fill_in 'branch-search', with: 'fix' - find('#branch-search').native.send_keys(:enter) - - expect(page).to have_content('fix') - expect(find('.all-branches')).to have_selector('li', count: 1) + it 'does not show merge request button' do + page.within first('.all-branches li') do + expect(page).not_to have_content 'Merge Request' + end end end end diff --git a/spec/features/builds_spec.rb b/spec/features/projects/builds_spec.rb index 0cfeb2e57d8..d1685f95503 100644 --- a/spec/features/builds_spec.rb +++ b/spec/features/projects/builds_spec.rb @@ -1,4 +1,5 @@ require 'spec_helper' +require 'tempfile' describe "Builds" do let(:artifacts_file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } @@ -6,7 +7,7 @@ describe "Builds" do before do login_as(:user) @commit = FactoryGirl.create :ci_pipeline - @build = FactoryGirl.create :ci_build, pipeline: @commit + @build = FactoryGirl.create :ci_build, :trace, pipeline: @commit @build2 = FactoryGirl.create :ci_build @project = @commit.project @project.team << [@user, :developer] @@ -156,7 +157,6 @@ describe "Builds" do context 'Build raw trace' do before do @build.run! - @build.trace = 'BUILD TRACE' visit namespace_project_build_path(@project.namespace, @project, @build) end @@ -164,6 +164,26 @@ describe "Builds" do expect(page).to have_link 'Raw' end end + + describe 'Variables' do + before do + @trigger_request = create :ci_trigger_request_with_variables + @build = create :ci_build, pipeline: @commit, trigger_request: @trigger_request + visit namespace_project_build_path(@project.namespace, @project, @build) + end + + it 'shows variable key and value after click', js: true do + expect(page).to have_css('.reveal-variables') + expect(page).not_to have_css('.js-build-variable') + expect(page).not_to have_css('.js-build-value') + + click_button 'Reveal Variables' + + expect(page).not_to have_css('.reveal-variables') + expect(page).to have_selector('.js-build-variable', text: 'TRIGGER_KEY_1') + expect(page).to have_selector('.js-build-value', text: 'TRIGGER_VALUE_1') + end + end end describe "POST /:project/builds/:id/cancel" do @@ -255,35 +275,101 @@ describe "Builds" do end end - describe "GET /:project/builds/:id/raw" do - context "Build from project" do - before do - Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile') - @build.run! - @build.trace = 'BUILD TRACE' - visit namespace_project_build_path(@project.namespace, @project, @build) - page.within('.js-build-sidebar') { click_link 'Raw' } + describe 'GET /:project/builds/:id/raw' do + context 'access source' do + context 'build from project' do + before do + Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile') + @build.run! + visit namespace_project_build_path(@project.namespace, @project, @build) + page.within('.js-build-sidebar') { click_link 'Raw' } + end + + it 'sends the right headers' do + expect(page.status_code).to eq(200) + expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8') + expect(page.response_headers['X-Sendfile']).to eq(@build.path_to_trace) + end end - it 'sends the right headers' do - expect(page.status_code).to eq(200) - expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8') - expect(page.response_headers['X-Sendfile']).to eq(@build.path_to_trace) + context 'build from other project' do + before do + Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile') + @build2.run! + visit raw_namespace_project_build_path(@project.namespace, @project, @build2) + end + + it 'sends the right headers' do + expect(page.status_code).to eq(404) + end end end - context "Build from other project" do - before do - Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile') - @build2.run! - @build2.trace = 'BUILD TRACE' - visit raw_namespace_project_build_path(@project.namespace, @project, @build2) - puts page.status_code - puts current_url + context 'storage form' do + let(:existing_file) { Tempfile.new('existing-trace-file').path } + let(:non_existing_file) do + file = Tempfile.new('non-existing-trace-file') + path = file.path + file.unlink + path end - it 'sends the right headers' do - expect(page.status_code).to eq(404) + context 'when build has trace in file' do + before do + Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile') + @build.run! + visit namespace_project_build_path(@project.namespace, @project, @build) + + allow_any_instance_of(Project).to receive(:ci_id).and_return(nil) + allow_any_instance_of(Ci::Build).to receive(:path_to_trace).and_return(existing_file) + allow_any_instance_of(Ci::Build).to receive(:old_path_to_trace).and_return(non_existing_file) + + page.within('.js-build-sidebar') { click_link 'Raw' } + end + + it 'sends the right headers' do + expect(page.status_code).to eq(200) + expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8') + expect(page.response_headers['X-Sendfile']).to eq(existing_file) + end + end + + context 'when build has trace in old file' do + before do + Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile') + @build.run! + visit namespace_project_build_path(@project.namespace, @project, @build) + + allow_any_instance_of(Project).to receive(:ci_id).and_return(999) + allow_any_instance_of(Ci::Build).to receive(:path_to_trace).and_return(non_existing_file) + allow_any_instance_of(Ci::Build).to receive(:old_path_to_trace).and_return(existing_file) + + page.within('.js-build-sidebar') { click_link 'Raw' } + end + + it 'sends the right headers' do + expect(page.status_code).to eq(200) + expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8') + expect(page.response_headers['X-Sendfile']).to eq(existing_file) + end + end + + context 'when build has trace in DB' do + before do + Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile') + @build.run! + visit namespace_project_build_path(@project.namespace, @project, @build) + + allow_any_instance_of(Project).to receive(:ci_id).and_return(nil) + allow_any_instance_of(Ci::Build).to receive(:path_to_trace).and_return(non_existing_file) + allow_any_instance_of(Ci::Build).to receive(:old_path_to_trace).and_return(non_existing_file) + + page.within('.js-build-sidebar') { click_link 'Raw' } + end + + it 'sends the right headers' do + expect(page.status_code).to eq(404) + end end end end diff --git a/spec/features/projects/edit_spec.rb b/spec/features/projects/edit_spec.rb new file mode 100644 index 00000000000..a1643fd1f43 --- /dev/null +++ b/spec/features/projects/edit_spec.rb @@ -0,0 +1,57 @@ +require 'rails_helper' + +feature 'Project edit', feature: true, js: true do + include WaitForAjax + + let(:user) { create(:user) } + let(:project) { create(:project) } + + before do + project.team << [user, :master] + login_as(user) + + visit edit_namespace_project_path(project.namespace, project) + end + + context 'feature visibility' do + context 'merge requests select' do + it 'hides merge requests section' do + select('Disabled', from: 'project_project_feature_attributes_merge_requests_access_level') + + expect(page).to have_selector('.merge-requests-feature', visible: false) + end + + it 'hides merge requests section after save' do + select('Disabled', from: 'project_project_feature_attributes_merge_requests_access_level') + + expect(page).to have_selector('.merge-requests-feature', visible: false) + + click_button 'Save changes' + + wait_for_ajax + + expect(page).to have_selector('.merge-requests-feature', visible: false) + end + end + + context 'builds select' do + it 'hides merge requests section' do + select('Disabled', from: 'project_project_feature_attributes_builds_access_level') + + expect(page).to have_selector('.builds-feature', visible: false) + end + + it 'hides merge requests section after save' do + select('Disabled', from: 'project_project_feature_attributes_builds_access_level') + + expect(page).to have_selector('.builds-feature', visible: false) + + click_button 'Save changes' + + wait_for_ajax + + expect(page).to have_selector('.builds-feature', visible: false) + end + end + end +end diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb new file mode 100644 index 00000000000..9b487e350f2 --- /dev/null +++ b/spec/features/projects/features_visibility_spec.rb @@ -0,0 +1,122 @@ +require 'spec_helper' +include WaitForAjax + +describe 'Edit Project Settings', feature: true do + let(:member) { create(:user) } + let!(:project) { create(:project, :public, path: 'gitlab', name: 'sample') } + let(:non_member) { create(:user) } + + describe 'project features visibility selectors', js: true do + before do + project.team << [member, :master] + login_as(member) + end + + tools = { builds: "pipelines", issues: "issues", wiki: "wiki", snippets: "snippets", merge_requests: "merge_requests" } + + tools.each do |tool_name, shortcut_name| + describe "feature #{tool_name}" do + it 'toggles visibility' do + visit edit_namespace_project_path(project.namespace, project) + + select 'Disabled', from: "project_project_feature_attributes_#{tool_name}_access_level" + click_button 'Save changes' + wait_for_ajax + expect(page).not_to have_selector(".shortcuts-#{shortcut_name}") + + select 'Everyone with access', from: "project_project_feature_attributes_#{tool_name}_access_level" + click_button 'Save changes' + wait_for_ajax + expect(page).to have_selector(".shortcuts-#{shortcut_name}") + + select 'Only team members', from: "project_project_feature_attributes_#{tool_name}_access_level" + click_button 'Save changes' + wait_for_ajax + expect(page).to have_selector(".shortcuts-#{shortcut_name}") + + sleep 0.1 + end + end + end + end + + describe 'project features visibility pages' do + before do + @tools = + { + builds: namespace_project_pipelines_path(project.namespace, project), + issues: namespace_project_issues_path(project.namespace, project), + wiki: namespace_project_wiki_path(project.namespace, project, :home), + snippets: namespace_project_snippets_path(project.namespace, project), + merge_requests: namespace_project_merge_requests_path(project.namespace, project), + } + end + + context 'normal user' do + it 'renders 200 if tool is enabled' do + @tools.each do |method_name, url| + project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::ENABLED) + visit url + expect(page.status_code).to eq(200) + end + end + + it 'renders 404 if feature is disabled' do + @tools.each do |method_name, url| + project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::DISABLED) + visit url + expect(page.status_code).to eq(404) + end + end + + it 'renders 404 if feature is enabled only for team members' do + project.team.truncate + + @tools.each do |method_name, url| + project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::PRIVATE) + visit url + expect(page.status_code).to eq(404) + end + end + + it 'renders 200 if users is member of group' do + group = create(:group) + project.group = group + project.save + + group.add_owner(member) + + @tools.each do |method_name, url| + project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::PRIVATE) + visit url + expect(page.status_code).to eq(200) + end + end + end + + context 'admin user' do + before do + non_member.update_attribute(:admin, true) + login_as(non_member) + end + + it 'renders 404 if feature is disabled' do + @tools.each do |method_name, url| + project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::DISABLED) + visit url + expect(page.status_code).to eq(404) + end + end + + it 'renders 200 if feature is enabled only for team members' do + project.team.truncate + + @tools.each do |method_name, url| + project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::PRIVATE) + visit url + expect(page.status_code).to eq(200) + end + end + end + end +end diff --git a/spec/features/projects/files/download_buttons_spec.rb b/spec/features/projects/files/download_buttons_spec.rb new file mode 100644 index 00000000000..be5cebcd7c9 --- /dev/null +++ b/spec/features/projects/files/download_buttons_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +feature 'Download buttons in files tree', feature: true do + given(:user) { create(:user) } + given(:role) { :developer } + given(:status) { 'success' } + given(:project) { create(:project) } + + given(:pipeline) do + create(:ci_pipeline, + project: project, + sha: project.commit.sha, + ref: project.default_branch, + status: status) + end + + given!(:build) do + create(:ci_build, :success, :artifacts, + pipeline: pipeline, + status: pipeline.status, + name: 'build') + end + + background do + login_as(user) + project.team << [user, role] + end + + describe 'when files tree' do + context 'with artifacts' do + before do + visit namespace_project_tree_path( + project.namespace, project, project.default_branch) + end + + scenario 'shows download artifacts button' do + expect(page).to have_link "Download '#{build.name}'" + end + end + end +end diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz Binary files differindex 7bb0d26b21c..e14b2705704 100644 --- a/spec/features/projects/import_export/test_project_export.tar.gz +++ b/spec/features/projects/import_export/test_project_export.tar.gz diff --git a/spec/features/projects/main/download_buttons_spec.rb b/spec/features/projects/main/download_buttons_spec.rb new file mode 100644 index 00000000000..b26c0ea7a14 --- /dev/null +++ b/spec/features/projects/main/download_buttons_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +feature 'Download buttons in project main page', feature: true do + given(:user) { create(:user) } + given(:role) { :developer } + given(:status) { 'success' } + given(:project) { create(:project) } + + given(:pipeline) do + create(:ci_pipeline, + project: project, + sha: project.commit.sha, + ref: project.default_branch, + status: status) + end + + given!(:build) do + create(:ci_build, :success, :artifacts, + pipeline: pipeline, + status: pipeline.status, + name: 'build') + end + + background do + login_as(user) + project.team << [user, role] + end + + describe 'when checking project main page' do + context 'with artifacts' do + before do + visit namespace_project_path(project.namespace, project) + end + + scenario 'shows download artifacts button' do + expect(page).to have_link "Download '#{build.name}'" + end + end + end +end diff --git a/spec/features/projects/tags/download_buttons_spec.rb b/spec/features/projects/tags/download_buttons_spec.rb new file mode 100644 index 00000000000..6e0022c179f --- /dev/null +++ b/spec/features/projects/tags/download_buttons_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +feature 'Download buttons in tags page', feature: true do + given(:user) { create(:user) } + given(:role) { :developer } + given(:status) { 'success' } + given(:tag) { 'v1.0.0' } + given(:project) { create(:project) } + + given(:pipeline) do + create(:ci_pipeline, + project: project, + sha: project.commit(tag).sha, + ref: tag, + status: status) + end + + given!(:build) do + create(:ci_build, :success, :artifacts, + pipeline: pipeline, + status: pipeline.status, + name: 'build') + end + + background do + login_as(user) + project.team << [user, role] + end + + describe 'when checking tags' do + context 'with artifacts' do + before do + visit namespace_project_tags_path(project.namespace, project) + end + + scenario 'shows download artifacts button' do + expect(page).to have_link "Download '#{build.name}'" + end + end + end +end diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb index 6ed279ef9be..abb27c90e0a 100644 --- a/spec/features/task_lists_spec.rb +++ b/spec/features/task_lists_spec.rb @@ -20,6 +20,22 @@ feature 'Task Lists', feature: true do MARKDOWN end + let(:singleIncompleteMarkdown) do + <<-MARKDOWN.strip_heredoc + This is a task list: + + - [ ] Incomplete entry 1 + MARKDOWN + end + + let(:singleCompleteMarkdown) do + <<-MARKDOWN.strip_heredoc + This is a task list: + + - [x] Incomplete entry 1 + MARKDOWN + end + before do Warden.test_mode! @@ -34,77 +50,145 @@ feature 'Task Lists', feature: true do end describe 'for Issues' do - let!(:issue) { create(:issue, description: markdown, author: user, project: project) } + describe 'multiple tasks' do + let!(:issue) { create(:issue, description: markdown, author: user, project: project) } - it 'renders' do - visit_issue(project, issue) + it 'renders' do + visit_issue(project, issue) - expect(page).to have_selector('ul.task-list', count: 1) - expect(page).to have_selector('li.task-list-item', count: 6) - expect(page).to have_selector('ul input[checked]', count: 2) - end + expect(page).to have_selector('ul.task-list', count: 1) + expect(page).to have_selector('li.task-list-item', count: 6) + expect(page).to have_selector('ul input[checked]', count: 2) + end + + it 'contains the required selectors' do + visit_issue(project, issue) + + container = '.detail-page-description .description.js-task-list-container' - it 'contains the required selectors' do - visit_issue(project, issue) + expect(page).to have_selector(container) + expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox") + expect(page).to have_selector("#{container} .js-task-list-field") + expect(page).to have_selector('form.js-issuable-update') + expect(page).to have_selector('a.btn-close') + end - container = '.detail-page-description .description.js-task-list-container' + it 'is only editable by author' do + visit_issue(project, issue) + expect(page).to have_selector('.js-task-list-container') - expect(page).to have_selector(container) - expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox") - expect(page).to have_selector("#{container} .js-task-list-field") - expect(page).to have_selector('form.js-issuable-update') - expect(page).to have_selector('a.btn-close') + logout(:user) + + login_as(user2) + visit current_path + expect(page).not_to have_selector('.js-task-list-container') + end + + it 'provides a summary on Issues#index' do + visit namespace_project_issues_path(project.namespace, project) + expect(page).to have_content("2 of 6 tasks completed") + end end - it 'is only editable by author' do - visit_issue(project, issue) - expect(page).to have_selector('.js-task-list-container') + describe 'single incomplete task' do + let!(:issue) { create(:issue, description: singleIncompleteMarkdown, author: user, project: project) } - logout(:user) + it 'renders' do + visit_issue(project, issue) - login_as(user2) - visit current_path - expect(page).not_to have_selector('.js-task-list-container') + expect(page).to have_selector('ul.task-list', count: 1) + expect(page).to have_selector('li.task-list-item', count: 1) + expect(page).to have_selector('ul input[checked]', count: 0) + end + + it 'provides a summary on Issues#index' do + visit namespace_project_issues_path(project.namespace, project) + expect(page).to have_content("0 of 1 task completed") + end end - it 'provides a summary on Issues#index' do - visit namespace_project_issues_path(project.namespace, project) - expect(page).to have_content("6 tasks (2 completed, 4 remaining)") + describe 'single complete task' do + let!(:issue) { create(:issue, description: singleCompleteMarkdown, author: user, project: project) } + + it 'renders' do + visit_issue(project, issue) + + expect(page).to have_selector('ul.task-list', count: 1) + expect(page).to have_selector('li.task-list-item', count: 1) + expect(page).to have_selector('ul input[checked]', count: 1) + end + + it 'provides a summary on Issues#index' do + visit namespace_project_issues_path(project.namespace, project) + expect(page).to have_content("1 of 1 task completed") + end end end describe 'for Notes' do let!(:issue) { create(:issue, author: user, project: project) } - let!(:note) do - create(:note, note: markdown, noteable: issue, - project: project, author: user) + describe 'multiple tasks' do + let!(:note) do + create(:note, note: markdown, noteable: issue, + project: project, author: user) + end + + it 'renders for note body' do + visit_issue(project, issue) + + expect(page).to have_selector('.note ul.task-list', count: 1) + expect(page).to have_selector('.note li.task-list-item', count: 6) + expect(page).to have_selector('.note ul input[checked]', count: 2) + end + + it 'contains the required selectors' do + visit_issue(project, issue) + + expect(page).to have_selector('.note .js-task-list-container') + expect(page).to have_selector('.note .js-task-list-container .task-list .task-list-item .task-list-item-checkbox') + expect(page).to have_selector('.note .js-task-list-container .js-task-list-field') + end + + it 'is only editable by author' do + visit_issue(project, issue) + expect(page).to have_selector('.js-task-list-container') + + logout(:user) + + login_as(user2) + visit current_path + expect(page).not_to have_selector('.js-task-list-container') + end end - it 'renders for note body' do - visit_issue(project, issue) - - expect(page).to have_selector('.note ul.task-list', count: 1) - expect(page).to have_selector('.note li.task-list-item', count: 6) - expect(page).to have_selector('.note ul input[checked]', count: 2) - end + describe 'single incomplete task' do + let!(:note) do + create(:note, note: singleIncompleteMarkdown, noteable: issue, + project: project, author: user) + end - it 'contains the required selectors' do - visit_issue(project, issue) + it 'renders for note body' do + visit_issue(project, issue) - expect(page).to have_selector('.note .js-task-list-container') - expect(page).to have_selector('.note .js-task-list-container .task-list .task-list-item .task-list-item-checkbox') - expect(page).to have_selector('.note .js-task-list-container .js-task-list-field') + expect(page).to have_selector('.note ul.task-list', count: 1) + expect(page).to have_selector('.note li.task-list-item', count: 1) + expect(page).to have_selector('.note ul input[checked]', count: 0) + end end - it 'is only editable by author' do - visit_issue(project, issue) - expect(page).to have_selector('.js-task-list-container') + describe 'single complete task' do + let!(:note) do + create(:note, note: singleCompleteMarkdown, noteable: issue, + project: project, author: user) + end - logout(:user) + it 'renders for note body' do + visit_issue(project, issue) - login_as(user2) - visit current_path - expect(page).not_to have_selector('.js-task-list-container') + expect(page).to have_selector('.note ul.task-list', count: 1) + expect(page).to have_selector('.note li.task-list-item', count: 1) + expect(page).to have_selector('.note ul input[checked]', count: 1) + end end end @@ -113,42 +197,78 @@ feature 'Task Lists', feature: true do visit namespace_project_merge_request_path(project.namespace, project, merge) end - let!(:merge) { create(:merge_request, :simple, description: markdown, author: user, source_project: project) } + describe 'multiple tasks' do + let!(:merge) { create(:merge_request, :simple, description: markdown, author: user, source_project: project) } - it 'renders for description' do - visit_merge_request(project, merge) + it 'renders for description' do + visit_merge_request(project, merge) - expect(page).to have_selector('ul.task-list', count: 1) - expect(page).to have_selector('li.task-list-item', count: 6) - expect(page).to have_selector('ul input[checked]', count: 2) - end + expect(page).to have_selector('ul.task-list', count: 1) + expect(page).to have_selector('li.task-list-item', count: 6) + expect(page).to have_selector('ul input[checked]', count: 2) + end - it 'contains the required selectors' do - visit_merge_request(project, merge) + it 'contains the required selectors' do + visit_merge_request(project, merge) - container = '.detail-page-description .description.js-task-list-container' + container = '.detail-page-description .description.js-task-list-container' - expect(page).to have_selector(container) - expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox") - expect(page).to have_selector("#{container} .js-task-list-field") - expect(page).to have_selector('form.js-issuable-update') - expect(page).to have_selector('a.btn-close') - end + expect(page).to have_selector(container) + expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox") + expect(page).to have_selector("#{container} .js-task-list-field") + expect(page).to have_selector('form.js-issuable-update') + expect(page).to have_selector('a.btn-close') + end - it 'is only editable by author' do - visit_merge_request(project, merge) - expect(page).to have_selector('.js-task-list-container') + it 'is only editable by author' do + visit_merge_request(project, merge) + expect(page).to have_selector('.js-task-list-container') - logout(:user) + logout(:user) - login_as(user2) - visit current_path - expect(page).not_to have_selector('.js-task-list-container') + login_as(user2) + visit current_path + expect(page).not_to have_selector('.js-task-list-container') + end + + it 'provides a summary on MergeRequests#index' do + visit namespace_project_merge_requests_path(project.namespace, project) + expect(page).to have_content("2 of 6 tasks completed") + end + end + + describe 'single incomplete task' do + let!(:merge) { create(:merge_request, :simple, description: singleIncompleteMarkdown, author: user, source_project: project) } + + it 'renders for description' do + visit_merge_request(project, merge) + + expect(page).to have_selector('ul.task-list', count: 1) + expect(page).to have_selector('li.task-list-item', count: 1) + expect(page).to have_selector('ul input[checked]', count: 0) + end + + it 'provides a summary on MergeRequests#index' do + visit namespace_project_merge_requests_path(project.namespace, project) + expect(page).to have_content("0 of 1 task completed") + end end - it 'provides a summary on MergeRequests#index' do - visit namespace_project_merge_requests_path(project.namespace, project) - expect(page).to have_content("6 tasks (2 completed, 4 remaining)") + describe 'single complete task' do + let!(:merge) { create(:merge_request, :simple, description: singleCompleteMarkdown, author: user, source_project: project) } + + it 'renders for description' do + visit_merge_request(project, merge) + + expect(page).to have_selector('ul.task-list', count: 1) + expect(page).to have_selector('li.task-list-item', count: 1) + expect(page).to have_selector('ul input[checked]', count: 1) + end + + it 'provides a summary on MergeRequests#index' do + visit namespace_project_merge_requests_path(project.namespace, project) + expect(page).to have_content("1 of 1 task completed") + end end end end diff --git a/spec/features/todos/todos_filtering_spec.rb b/spec/features/todos/todos_filtering_spec.rb new file mode 100644 index 00000000000..83cf306437d --- /dev/null +++ b/spec/features/todos/todos_filtering_spec.rb @@ -0,0 +1,63 @@ +require 'spec_helper' + +describe 'Dashboard > User filters todos', feature: true, js: true do + include WaitForAjax + + let(:user_1) { create(:user, username: 'user_1', name: 'user_1') } + let(:user_2) { create(:user, username: 'user_2', name: 'user_2') } + + let(:project_1) { create(:empty_project, name: 'project_1') } + let(:project_2) { create(:empty_project, name: 'project_2') } + + let(:issue) { create(:issue, title: 'issue', project: project_1) } + + let!(:merge_request) { create(:merge_request, source_project: project_2, title: 'merge_request') } + + before do + create(:todo, user: user_1, author: user_2, project: project_1, target: issue, action: 1) + create(:todo, user: user_1, author: user_1, project: project_2, target: merge_request, action: 2) + + project_1.team << [user_1, :developer] + project_2.team << [user_1, :developer] + login_as(user_1) + visit dashboard_todos_path + end + + it 'filters by project' do + click_button 'Project' + within '.dropdown-menu-project' do + fill_in 'Search projects', with: project_1.name_with_namespace + click_link project_1.name_with_namespace + end + wait_for_ajax + expect('.prepend-top-default').not_to have_content project_2.name_with_namespace + end + + it 'filters by author' do + click_button 'Author' + within '.dropdown-menu-author' do + fill_in 'Search authors', with: user_1.name + click_link user_1.name + end + wait_for_ajax + expect('.prepend-top-default').not_to have_content user_2.name + end + + it 'filters by type' do + click_button 'Type' + within '.dropdown-menu-type' do + click_link 'Issue' + end + wait_for_ajax + expect('.prepend-top-default').not_to have_content ' merge request !' + end + + it 'filters by action' do + click_button 'Action' + within '.dropdown-menu-action' do + click_link 'Assigned' + end + wait_for_ajax + expect('.prepend-top-default').not_to have_content ' mentioned ' + end +end diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb index 0342f4f1d97..fc555a74f30 100644 --- a/spec/features/todos/todos_spec.rb +++ b/spec/features/todos/todos_spec.rb @@ -41,6 +41,27 @@ describe 'Dashboard Todos', feature: true do expect(page).to have_content("You're all done!") end end + + context 'todo is stale on the page' do + before do + todos = TodosFinder.new(user, state: :pending).execute + TodoService.new.mark_todos_as_done(todos, user) + end + + describe 'deleting the todo' do + before do + first('.done-todo').click + end + + it 'is removed from the list' do + expect(page).not_to have_selector('.todos-list .todo') + end + + it 'shows "All done" message' do + expect(page).to have_content("You're all done!") + end + end + end end context 'User has Todos with labels spanning multiple projects' do @@ -97,6 +118,20 @@ describe 'Dashboard Todos', feature: true do expect(page).to have_css("#todo_#{Todo.first.id}") end end + + describe 'mark all as done', js: true do + before do + visit dashboard_todos_path + click_link('Mark all as done') + end + + it 'shows "All done" message!' do + within('.todos-pending-count') { expect(page).to have_content '0' } + expect(page).to have_content 'To do 0' + expect(page).to have_content "You're all done!" + expect(page).not_to have_selector('.gl-pagination') + end + end end context 'User has a Todo in a project pending deletion' do diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb index 3cbc8253ad6..72354834c5a 100644 --- a/spec/features/triggers_spec.rb +++ b/spec/features/triggers_spec.rb @@ -12,7 +12,7 @@ describe 'Triggers' do context 'create a trigger' do before do - click_on 'Add Trigger' + click_on 'Add trigger' expect(@project.triggers.count).to eq(1) end diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb index a46e48c76ed..ff6933dc8d9 100644 --- a/spec/features/u2f_spec.rb +++ b/spec/features/u2f_spec.rb @@ -156,6 +156,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: describe "when 2FA via OTP is disabled" do it "allows logging in with the U2F device" do + user.update_attribute(:otp_required_for_login, false) login_with(user) @u2f_device.respond_to_u2f_authentication @@ -181,6 +182,19 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: end end + it 'persists remember_me value via hidden field' do + login_with(user, remember: true) + + @u2f_device.respond_to_u2f_authentication + click_on "Login Via U2F Device" + expect(page.body).to match('We heard back from your U2F device') + + within 'div#js-authenticate-u2f' do + field = first('input#user_remember_me', visible: false) + expect(field.value).to eq '1' + end + end + describe "when a given U2F device has already been registered by another user" do describe "but not the current user" do it "does not allow logging in with that particular device" do diff --git a/spec/features/users/snippets_spec.rb b/spec/features/users/snippets_spec.rb new file mode 100644 index 00000000000..356a8d668b0 --- /dev/null +++ b/spec/features/users/snippets_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe 'Snippets tab on a user profile', feature: true, js: true do + include WaitForAjax + + let(:user) { create(:user) } + + context 'when the user has snippets' do + before do + create_list(:snippet, 25, :public, author: user) + + visit user_path(user) + page.within('.user-profile-nav') { click_link 'Snippets' } + wait_for_ajax + end + + it 'is limited to 20 items per page' do + expect(page.all('.snippets-list-holder .snippet-row').count).to eq(20) + end + + context 'clicking on the link to the second page' do + before { click_link('2') } + + it 'shows the remaining snippets' do + expect(page.all('.snippets-list-holder .snippet-row').count).to eq(5) + end + end + end +end diff --git a/spec/finders/pipelines_finder_spec.rb b/spec/finders/pipelines_finder_spec.rb new file mode 100644 index 00000000000..b0811d134fa --- /dev/null +++ b/spec/finders/pipelines_finder_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +describe PipelinesFinder do + let(:project) { create(:project) } + + let!(:tag_pipeline) { create(:ci_pipeline, project: project, ref: 'v1.0.0') } + let!(:branch_pipeline) { create(:ci_pipeline, project: project) } + + subject { described_class.new(project).execute(params) } + + describe "#execute" do + context 'when a scope is passed' do + context 'when scope is nil' do + let(:params) { { scope: nil } } + + it 'selects all pipelines' do + expect(subject.count).to be 2 + expect(subject).to include tag_pipeline + expect(subject).to include branch_pipeline + end + end + + context 'when selecting branches' do + let(:params) { { scope: 'branches' } } + + it 'excludes tags' do + expect(subject).not_to include tag_pipeline + expect(subject).to include branch_pipeline + end + end + + context 'when selecting tags' do + let(:params) { { scope: 'tags' } } + + it 'excludes branches' do + expect(subject).to include tag_pipeline + expect(subject).not_to include branch_pipeline + end + end + end + + # Scoping to running will speed up the test as it doesn't hit the FS + let(:params) { { scope: 'running' } } + + it 'orders in descending order on ID' do + feature_pipeline = create(:ci_pipeline, project: project, ref: 'feature') + + expected_ids = [feature_pipeline.id, branch_pipeline.id, tag_pipeline.id].sort.reverse + expect(subject.map(&:id)).to eq expected_ids + end + end +end diff --git a/spec/finders/tags_finder_spec.rb b/spec/finders/tags_finder_spec.rb new file mode 100644 index 00000000000..2ac810e478a --- /dev/null +++ b/spec/finders/tags_finder_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' + +describe TagsFinder do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:repository) { project.repository } + + describe '#execute' do + context 'sort only' do + it 'sorts by name' do + tags_finder = described_class.new(repository, {}) + + result = tags_finder.execute + + expect(result.first.name).to eq("v1.0.0") + end + + it 'sorts by recently_updated' do + tags_finder = described_class.new(repository, { sort: 'updated_desc' }) + + result = tags_finder.execute + recently_updated_tag = repository.tags.max do |a, b| + repository.commit(a.target).committed_date <=> repository.commit(b.target).committed_date + end + + expect(result.first.name).to eq(recently_updated_tag.name) + end + + it 'sorts by last_updated' do + tags_finder = described_class.new(repository, { sort: 'updated_asc' }) + + result = tags_finder.execute + + expect(result.first.name).to eq('v1.0.0') + end + end + + context 'filter only' do + it 'filters tags by name' do + tags_finder = described_class.new(repository, { search: '1.0.0' }) + + result = tags_finder.execute + + expect(result.first.name).to eq('v1.0.0') + expect(result.count).to eq(1) + end + + it 'does not find any tags with that name' do + tags_finder = described_class.new(repository, { search: 'hey' }) + + result = tags_finder.execute + + expect(result.count).to eq(0) + end + end + + context 'filter and sort' do + it 'filters tags by name and sorts by recently_updated' do + params = { sort: 'updated_desc', search: 'v1' } + tags_finder = described_class.new(repository, params) + + result = tags_finder.execute + + expect(result.first.name).to eq('v1.1.0') + expect(result.count).to eq(2) + end + + it 'filters tags by name and sorts by last_updated' do + params = { sort: 'updated_asc', search: 'v1' } + tags_finder = described_class.new(repository, params) + + result = tags_finder.execute + + expect(result.first.name).to eq('v1.0.0') + expect(result.count).to eq(2) + end + end + end +end diff --git a/spec/fixtures/api/schemas/issues.json b/spec/fixtures/api/schemas/issues.json index 0d2067f704a..70771b21c96 100644 --- a/spec/fixtures/api/schemas/issues.json +++ b/spec/fixtures/api/schemas/issues.json @@ -1,4 +1,15 @@ { - "type": "array", - "items": { "$ref": "issue.json" } + "type": "object", + "required" : [ + "issues", + "size" + ], + "properties" : { + "issues": { + "type": "array", + "items": { "$ref": "issue.json" } + }, + "size": { "type": "integer" } + }, + "additionalProperties": false } diff --git a/spec/helpers/git_helper_spec.rb b/spec/helpers/git_helper_spec.rb new file mode 100644 index 00000000000..9b1ef1e05a2 --- /dev/null +++ b/spec/helpers/git_helper_spec.rb @@ -0,0 +1,9 @@ +require 'spec_helper' + +describe GitHelper do + describe '#short_sha' do + let(:short_sha) { helper.short_sha('d4e043f6c20749a3ab3f4b8e23f2a8979f4b9100') } + + it { expect(short_sha).to eq('d4e043f6') } + end +end diff --git a/spec/helpers/import_helper_spec.rb b/spec/helpers/import_helper_spec.rb index 3391234e9f5..187b891b927 100644 --- a/spec/helpers/import_helper_spec.rb +++ b/spec/helpers/import_helper_spec.rb @@ -1,6 +1,30 @@ require 'rails_helper' describe ImportHelper do + describe '#import_project_target' do + let(:user) { create(:user) } + + before do + allow(helper).to receive(:current_user).and_return(user) + end + + context 'when current user can create namespaces' do + it 'returns project namespace' do + user.update_attribute(:can_create_group, true) + + expect(helper.import_project_target('asd', 'vim')).to eq 'asd/vim' + end + end + + context 'when current user can not create namespaces' do + it "takes the current user's namespace" do + user.update_attribute(:can_create_group, false) + + expect(helper.import_project_target('asd', 'vim')).to eq "#{user.namespace_path}/vim" + end + end + end + describe '#github_project_link' do context 'when provider does not specify a custom URL' do it 'uses default GitHub URL' do diff --git a/spec/helpers/nav_helper_spec.rb b/spec/helpers/nav_helper_spec.rb deleted file mode 100644 index e4d18d8bfc6..00000000000 --- a/spec/helpers/nav_helper_spec.rb +++ /dev/null @@ -1,25 +0,0 @@ -require 'spec_helper' - -# Specs in this file have access to a helper object that includes -# the NavHelper. For example: -# -# describe NavHelper do -# describe "string concat" do -# it "concats two strings with spaces" do -# expect(helper.concat_strings("this","that")).to eq("this that") -# end -# end -# end -describe NavHelper do - describe '#nav_menu_collapsed?' do - it 'returns true when the nav is collapsed in the cookie' do - helper.request.cookies[:collapsed_nav] = 'true' - expect(helper.nav_menu_collapsed?).to eq true - end - - it 'returns false when the nav is not collapsed in the cookie' do - helper.request.cookies[:collapsed_nav] = 'false' - expect(helper.nav_menu_collapsed?).to eq false - end - end -end diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 284b58d8d5c..70032e7df94 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -174,4 +174,48 @@ describe ProjectsHelper do end end end + + describe "#project_feature_access_select" do + let(:project) { create(:empty_project, :public) } + let(:user) { create(:user) } + + context "when project is internal or public" do + it "shows all options" do + helper.instance_variable_set(:@project, project) + result = helper.project_feature_access_select(:issues_access_level) + expect(result).to include("Disabled") + expect(result).to include("Only team members") + expect(result).to include("Everyone with access") + end + end + + context "when project is private" do + before { project.update_attributes(visibility_level: Gitlab::VisibilityLevel::PRIVATE) } + + it "shows only allowed options" do + helper.instance_variable_set(:@project, project) + result = helper.project_feature_access_select(:issues_access_level) + expect(result).to include("Disabled") + expect(result).to include("Only team members") + expect(result).not_to include("Everyone with access") + end + end + + context "when project moves from public to private" do + before do + project.project_feature.update_attributes(issues_access_level: ProjectFeature::ENABLED) + project.update_attributes(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + + it "shows the highest allowed level selected" do + helper.instance_variable_set(:@project, project) + result = helper.project_feature_access_select(:issues_access_level) + + expect(result).to include("Disabled") + expect(result).to include("Only team members") + expect(result).not_to include("Everyone with access") + expect(result).to have_selector('option[selected]', text: "Only team members") + end + end + end end diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb index b0bb991539b..4b2ca3514f8 100644 --- a/spec/helpers/search_helper_spec.rb +++ b/spec/helpers/search_helper_spec.rb @@ -32,6 +32,10 @@ describe SearchHelper do expect(search_autocomplete_opts("adm").size).to eq(1) end + it "does not allow regular expression in search term" do + expect(search_autocomplete_opts("(webhooks|api)").size).to eq(0) + end + it "includes the user's groups" do create(:group).add_owner(user) expect(search_autocomplete_opts("gro").size).to eq(1) diff --git a/spec/helpers/sidekiq_helper_spec.rb b/spec/helpers/sidekiq_helper_spec.rb new file mode 100644 index 00000000000..d60839b78ec --- /dev/null +++ b/spec/helpers/sidekiq_helper_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe SidekiqHelper do + describe 'parse_sidekiq_ps' do + it 'parses line with time' do + line = '55137 10,0 2,1 S+ 2:30pm sidekiq 4.1.4 gitlab [0 of 25 busy] ' + parts = helper.parse_sidekiq_ps(line) + + expect(parts).to eq(['55137', '10,0', '2,1', 'S+', '2:30pm', 'sidekiq 4.1.4 gitlab [0 of 25 busy]']) + end + + it 'parses line with date' do + line = '55137 10,0 2,1 S+ Aug 4 sidekiq 4.1.4 gitlab [0 of 25 busy] ' + parts = helper.parse_sidekiq_ps(line) + + expect(parts).to eq(['55137', '10,0', '2,1', 'S+', 'Aug 4', 'sidekiq 4.1.4 gitlab [0 of 25 busy]']) + end + + it 'parses line with two digit date' do + line = '55137 10,0 2,1 S+ Aug 04 sidekiq 4.1.4 gitlab [0 of 25 busy] ' + parts = helper.parse_sidekiq_ps(line) + + expect(parts).to eq(['55137', '10,0', '2,1', 'S+', 'Aug 04', 'sidekiq 4.1.4 gitlab [0 of 25 busy]']) + end + + it 'parses line with dot as float separator' do + line = '55137 10.0 2.1 S+ 2:30pm sidekiq 4.1.4 gitlab [0 of 25 busy] ' + parts = helper.parse_sidekiq_ps(line) + + expect(parts).to eq(['55137', '10.0', '2.1', 'S+', '2:30pm', 'sidekiq 4.1.4 gitlab [0 of 25 busy]']) + end + + it 'does fail gracefully on line not matching the format' do + line = '55137 10.0 2.1 S+ 2:30pm something' + parts = helper.parse_sidekiq_ps(line) + + expect(parts).to eq(['?', '?', '?', '?', '?', '?']) + end + end +end diff --git a/spec/javascripts/application_spec.js b/spec/javascripts/application_spec.js index b48026c3b77..56b98856614 100644 --- a/spec/javascripts/application_spec.js +++ b/spec/javascripts/application_spec.js @@ -13,17 +13,21 @@ gl.utils.preventDisabledButtons(); isClicked = false; $button = $('#test-button'); + expect($button).toExist(); $button.click(function() { return isClicked = true; }); $button.trigger('click'); return expect(isClicked).toBe(false); }); - return it('should be on the same page if a disabled link clicked', function() { - var locationBeforeLinkClick; + + it('should be on the same page if a disabled link clicked', function() { + var locationBeforeLinkClick, $link; locationBeforeLinkClick = window.location.href; gl.utils.preventDisabledButtons(); - $('#test-link').click(); + $link = $('#test-link'); + expect($link).toExist(); + $link.click(); return expect(window.location.href).toBe(locationBeforeLinkClick); }); }); diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index fa32d0d7da5..019ce3b0702 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -1,17 +1,11 @@ /*= require awards_handler */ - - /*= require jquery */ - - /*= require jquery.cookie */ - - /*= require ./fixtures/emoji_menu */ (function() { - var awardsHandler, lazyAssert; + var awardsHandler, lazyAssert, urlRoot; awardsHandler = null; @@ -27,11 +21,13 @@ }; gon.award_menu_url = '/emojis'; + urlRoot = gon.relative_url_root; lazyAssert = function(done, assertFn) { return setTimeout(function() { assertFn(); return done(); + // Maybe jasmine.clock here? }, 333); }; @@ -45,9 +41,14 @@ return cb(); }; })(this)); - return spyOn(jQuery, 'get').and.callFake(function(req, cb) { + spyOn(jQuery, 'get').and.callFake(function(req, cb) { return cb(window.emojiMenu); }); + spyOn(jQuery, 'cookie'); + }); + afterEach(function() { + // restore original url root value + gon.relative_url_root = urlRoot; }); describe('::showEmojiMenu', function() { it('should show emoji menu when Add emoji button clicked', function(done) { @@ -189,6 +190,28 @@ return expect($thumbsUpEmoji.data("original-title")).toBe('sam'); }); }); + describe('::addEmojiToFrequentlyUsedList', function() { + it('should set a cookie with the correct default path', function() { + gon.relative_url_root = ''; + awardsHandler.addEmojiToFrequentlyUsedList('sunglasses'); + expect(jQuery.cookie) + .toHaveBeenCalledWith('frequently_used_emojis', 'sunglasses', { + path: '/', + expires: 365 + }) + ; + }); + it('should set a cookie with the correct custom root path', function() { + gon.relative_url_root = '/gitlab/subdir'; + awardsHandler.addEmojiToFrequentlyUsedList('alien'); + expect(jQuery.cookie) + .toHaveBeenCalledWith('frequently_used_emojis', 'alien', { + path: '/gitlab/subdir', + expires: 365 + }) + ; + }); + }); describe('search', function() { return it('should filter the emoji', function() { $('.js-add-award').eq(0).click(); diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js index 4c52ecd903d..13babb5bfdb 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js +++ b/spec/javascripts/behaviors/quick_submit_spec.js @@ -8,6 +8,7 @@ beforeEach(function() { fixture.load('behaviors/quick_submit.html'); $('form').submit(function(e) { + // Prevent a form submit from moving us off the testing page return e.preventDefault(); }); return this.spies = { @@ -38,6 +39,8 @@ expect($('input[type=submit]')).toBeDisabled(); return expect($('button[type=submit]')).toBeDisabled(); }); + // We cannot stub `navigator.userAgent` for CI's `rake teaspoon` task, so we'll + // only run the tests that apply to the current platform if (navigator.userAgent.match(/Macintosh/)) { it('responds to Meta+Enter', function() { $('input.quick-submit-input').trigger(keydownEvent()); diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6 index c206b794442..1688b996162 100644 --- a/spec/javascripts/boards/list_spec.js.es6 +++ b/spec/javascripts/boards/list_spec.js.es6 @@ -60,15 +60,6 @@ describe('List model', () => { }, 0); }); - it('can\'t search when not backlog', () => { - expect(list.canSearch()).toBe(false); - }); - - it('can search when backlog', () => { - list.type = 'backlog'; - expect(list.canSearch()).toBe(true); - }); - it('gets issue from list', (done) => { setTimeout(() => { const issue = list.findIssue(1); diff --git a/spec/javascripts/boards/mock_data.js.es6 b/spec/javascripts/boards/mock_data.js.es6 index 0c37ec8354f..f3797ed44d4 100644 --- a/spec/javascripts/boards/mock_data.js.es6 +++ b/spec/javascripts/boards/mock_data.js.es6 @@ -26,12 +26,15 @@ const listObjDuplicate = { const BoardsMockData = { 'GET': { - '/test/issue-boards/board/lists{/id}/issues': [{ - title: 'Testing', - iid: 1, - confidential: false, - labels: [] - }] + '/test/issue-boards/board/lists{/id}/issues': { + issues: [{ + title: 'Testing', + iid: 1, + confidential: false, + labels: [] + }], + size: 1 + } }, 'POST': { '/test/issue-boards/board/lists{/id}': listObj diff --git a/spec/javascripts/datetime_utility_spec.js.coffee b/spec/javascripts/datetime_utility_spec.js.coffee deleted file mode 100644 index 6b9617341fe..00000000000 --- a/spec/javascripts/datetime_utility_spec.js.coffee +++ /dev/null @@ -1,31 +0,0 @@ -#= require lib/utils/datetime_utility - -describe 'Date time utils', -> - describe 'get day name', -> - it 'should return Sunday', -> - day = gl.utils.getDayName(new Date('07/17/2016')) - expect(day).toBe('Sunday') - - it 'should return Monday', -> - day = gl.utils.getDayName(new Date('07/18/2016')) - expect(day).toBe('Monday') - - it 'should return Tuesday', -> - day = gl.utils.getDayName(new Date('07/19/2016')) - expect(day).toBe('Tuesday') - - it 'should return Wednesday', -> - day = gl.utils.getDayName(new Date('07/20/2016')) - expect(day).toBe('Wednesday') - - it 'should return Thursday', -> - day = gl.utils.getDayName(new Date('07/21/2016')) - expect(day).toBe('Thursday') - - it 'should return Friday', -> - day = gl.utils.getDayName(new Date('07/22/2016')) - expect(day).toBe('Friday') - - it 'should return Saturday', -> - day = gl.utils.getDayName(new Date('07/23/2016')) - expect(day).toBe('Saturday') diff --git a/spec/javascripts/datetime_utility_spec.js.es6 b/spec/javascripts/datetime_utility_spec.js.es6 new file mode 100644 index 00000000000..a2d1b0a7732 --- /dev/null +++ b/spec/javascripts/datetime_utility_spec.js.es6 @@ -0,0 +1,64 @@ +//= require lib/utils/datetime_utility +(() => { + describe('Date time utils', () => { + describe('get day name', () => { + it('should return Sunday', () => { + const day = gl.utils.getDayName(new Date('07/17/2016')); + expect(day).toBe('Sunday'); + }); + + it('should return Monday', () => { + const day = gl.utils.getDayName(new Date('07/18/2016')); + expect(day).toBe('Monday'); + }); + + it('should return Tuesday', () => { + const day = gl.utils.getDayName(new Date('07/19/2016')); + expect(day).toBe('Tuesday'); + }); + + it('should return Wednesday', () => { + const day = gl.utils.getDayName(new Date('07/20/2016')); + expect(day).toBe('Wednesday'); + }); + + it('should return Thursday', () => { + const day = gl.utils.getDayName(new Date('07/21/2016')); + expect(day).toBe('Thursday'); + }); + + it('should return Friday', () => { + const day = gl.utils.getDayName(new Date('07/22/2016')); + expect(day).toBe('Friday'); + }); + + it('should return Saturday', () => { + const day = gl.utils.getDayName(new Date('07/23/2016')); + expect(day).toBe('Saturday'); + }); + }); + + describe('get day difference', () => { + it('should return 7', () => { + const firstDay = new Date('07/01/2016'); + const secondDay = new Date('07/08/2016'); + const difference = gl.utils.getDayDifference(firstDay, secondDay); + expect(difference).toBe(7); + }); + + it('should return 31', () => { + const firstDay = new Date('07/01/2016'); + const secondDay = new Date('08/01/2016'); + const difference = gl.utils.getDayDifference(firstDay, secondDay); + expect(difference).toBe(31); + }); + + it('should return 365', () => { + const firstDay = new Date('07/02/2015'); + const secondDay = new Date('07/01/2016'); + const difference = gl.utils.getDayDifference(firstDay, secondDay); + expect(difference).toBe(365); + }); + }); + }); +})(); diff --git a/spec/javascripts/fixtures/awards_handler.html.haml b/spec/javascripts/fixtures/awards_handler.html.haml index d55936ee4f9..1ef2e8f8624 100644 --- a/spec/javascripts/fixtures/awards_handler.html.haml +++ b/spec/javascripts/fixtures/awards_handler.html.haml @@ -39,7 +39,7 @@ %span.note-role Reporter %a.note-action-button.note-emoji-button.js-add-award.js-note-emoji{"data-position" => "right", :href => "#", :title => "Award Emoji"} %i.fa.fa-spinner.fa-spin - %i.fa.fa-smile-o + %i.fa.fa-smile-o.link-highlight .js-task-list-container.note-body.is-task-list-enabled .note-text %p Suscipit sunt quia quisquam sed eveniet ipsam. diff --git a/spec/javascripts/fixtures/comments.html.haml b/spec/javascripts/fixtures/comments.html.haml new file mode 100644 index 00000000000..cc1f8f15c21 --- /dev/null +++ b/spec/javascripts/fixtures/comments.html.haml @@ -0,0 +1,21 @@ +.flash-container.timeline-content +.timeline-icon.hidden-xs.hidden-sm + %a.author_link + %img +.timeline-content.timeline-content-form + %form.new-note.js-quick-submit.common-note-form.gfm-form.js-main-target-form + .md-area + .md-header + .md-write-holder + .zen-backdrop.div-dropzone-wrapper + .div-dropzone-wrapper + .div-dropzone.dz-clickable + %textarea.note-textarea.js-note-text.js-gfm-input.js-autosize.markdown-area + .note-form-actions.clearfix + %input.btn.btn-nr.btn-create.append-right-10.comment-btn.js-comment-button{ type: 'submit' } + %a.btn.btn-nr.btn-reopen.btn-comment.js-note-target-reopen + Reopen issue + %a.btn.btn-nr.btn-close.btn-comment.js-note-target-close + Close issue + %a.btn.btn-cancel.js-note-discard + Discard draft
\ No newline at end of file diff --git a/spec/javascripts/fixtures/u2f/authenticate.html.haml b/spec/javascripts/fixtures/u2f/authenticate.html.haml index 859e79a6c9e..779d6429a5f 100644 --- a/spec/javascripts/fixtures/u2f/authenticate.html.haml +++ b/spec/javascripts/fixtures/u2f/authenticate.html.haml @@ -1 +1 @@ -= render partial: "u2f/authenticate", locals: { new_user_session_path: "/users/sign_in" } += render partial: "u2f/authenticate", locals: { new_user_session_path: "/users/sign_in", params: {}, resource_name: "user" } diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js index 82ee1954a59..d5401fbb0d1 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js @@ -7,7 +7,7 @@ describe("ContributorsGraph", function () { expect(ContributorsGraph.prototype.x_domain).toEqual(20) }) }) - + describe("#set_y_domain", function () { it("sets the y_domain", function () { ContributorsGraph.set_y_domain([{commits: 30}]) @@ -89,7 +89,7 @@ describe("ContributorsGraph", function () { }) describe("ContributorsMasterGraph", function () { - + // TODO: fix or remove //describe("#process_dates", function () { //it("gets and parses dates", function () { @@ -103,7 +103,7 @@ describe("ContributorsMasterGraph", function () { //expect(graph.get_dates).toHaveBeenCalledWith(data) //expect(ContributorsGraph.set_dates).toHaveBeenCalledWith("get") //}) - //}) + //}) describe("#get_dates", function () { it("plucks the date field from data collection", function () { @@ -124,5 +124,5 @@ describe("ContributorsMasterGraph", function () { }) }) - + }) diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index dc6231ebb38..33690c7a5f3 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -1,7 +1,5 @@ /*= require lib/utils/text_utility */ - - /*= require issue */ (function() { diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js index 25d3f5b6c04..f09596bd36d 100644 --- a/spec/javascripts/new_branch_spec.js +++ b/spec/javascripts/new_branch_spec.js @@ -1,7 +1,5 @@ /*= require jquery-ui/autocomplete */ - - /*= require new_branch_form */ (function() { diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 14dc6bfdfde..a588f403dd5 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -1,8 +1,7 @@ - /*= require notes */ - - +/*= require autosize */ /*= require gl_form */ +/*= require lib/utils/text_utility */ (function() { window.gon || (window.gon = {}); @@ -12,29 +11,63 @@ }; describe('Notes', function() { - return describe('task lists', function() { + describe('task lists', function() { fixture.preload('issue_note.html'); + beforeEach(function() { fixture.load('issue_note.html'); $('form').on('submit', function(e) { - return e.preventDefault(); + e.preventDefault(); }); - return this.notes = new Notes(); + this.notes = new Notes(); }); + it('modifies the Markdown field', function() { $('input[type=checkbox]').attr('checked', true).trigger('change'); - return expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); + expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); }); - return it('submits the form on tasklist:changed', function() { - var submitted; - submitted = false; + + it('submits the form on tasklist:changed', function() { + var submitted = false; $('form').on('submit', function(e) { submitted = true; - return e.preventDefault(); + e.preventDefault(); }); + $('.js-task-list-field').trigger('tasklist:changed'); - return expect(submitted).toBe(true); + expect(submitted).toBe(true); + }); + }); + + describe('comments', function() { + var commentsTemplate = 'comments.html'; + var textarea = '.js-note-text'; + fixture.preload(commentsTemplate); + + beforeEach(function() { + fixture.load(commentsTemplate); + this.notes = new Notes(); + + this.autoSizeSpy = spyOnEvent($(textarea), 'autosize:update'); + spyOn(this.notes, 'renderNote').and.stub(); + + $(textarea).data('autosave', { + reset: function() {} + }); + + $('form').on('submit', function(e) { + e.preventDefault(); + $('.js-main-target-form').trigger('ajax:success'); + }); }); + + it('autosizes after comment submission', function() { + $(textarea).text('This is an example comment note'); + expect(this.autoSizeSpy).not.toHaveBeenTriggered(); + + $('.js-comment-button').click(); + expect(this.autoSizeSpy).toHaveBeenTriggered(); + }) }); }); diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js index ffe49828492..51eb12b41d4 100644 --- a/spec/javascripts/project_title_spec.js +++ b/spec/javascripts/project_title_spec.js @@ -1,22 +1,10 @@ /*= require bootstrap */ - - /*= require select2 */ - - /*= require lib/utils/type_utility */ - - /*= require gl_dropdown */ - - /*= require api */ - - /*= require project_select */ - - /*= require project */ (function() { diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index 38b3b2653ec..c937a4706f7 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -1,10 +1,6 @@ /*= require right_sidebar */ - - /*= require jquery */ - - /*= require jquery.cookie */ (function() { diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index 324f5152780..00d9fc1302a 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -1,19 +1,9 @@ /*= require gl_dropdown */ - - /*= require search_autocomplete */ - - /*= require jquery */ - - /*= require lib/utils/common_utils */ - - /*= require lib/utils/type_utility */ - - /*= require fuzzaldrin-plus */ (function() { @@ -43,6 +33,8 @@ groupName = 'Gitlab Org'; + // Add required attributes to body before starting the test. + // section would be dashboard|group|project addBodyAttributes = function(section) { var $body; if (section == null) { @@ -64,6 +56,7 @@ } }; + // Mock `gl` object in window for dashboard specific page. App code will need it. mockDashboardOptions = function() { window.gl || (window.gl = {}); return window.gl.dashboardOptions = { @@ -72,6 +65,7 @@ }; }; + // Mock `gl` object in window for project specific page. App code will need it. mockProjectOptions = function() { window.gl || (window.gl = {}); return window.gl.projectOptions = { diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index 7b6b55fe545..04ccf246052 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -10,6 +10,7 @@ }); return describe('#replyWithSelectedText', function() { var stubSelection; + // Stub window.getSelection to return the provided String. stubSelection = function(text) { return window.getSelection = function() { return text; diff --git a/spec/javascripts/spec_helper.js b/spec/javascripts/spec_helper.js index 7d91ed0f855..8801c297887 100644 --- a/spec/javascripts/spec_helper.js +++ b/spec/javascripts/spec_helper.js @@ -1,21 +1,41 @@ - +// PhantomJS (Teaspoons default driver) doesn't have support for +// Function.prototype.bind, which has caused confusion. Use this polyfill to +// avoid the confusion. /*= require support/bind-poly */ - +// You can require your own javascript files here. By default this will include +// everything in application, however you may get better load performance if you +// require the specific files that are being used in the spec that tests them. /*= require jquery */ - - /*= require jquery.turbolinks */ - - /*= require bootstrap */ - - /*= require underscore */ - +// Teaspoon includes some support files, but you can use anything from your own +// support path too. +// require support/jasmine-jquery-1.7.0 +// require support/jasmine-jquery-2.0.0 /*= require support/jasmine-jquery-2.1.0 */ +// require support/sinon +// require support/your-support-file +// Deferring execution +// If you're using CommonJS, RequireJS or some other asynchronous library you can +// defer execution. Call Teaspoon.execute() after everything has been loaded. +// Simple example of a timeout: +// Teaspoon.defer = true +// setTimeout(Teaspoon.execute, 1000) +// Matching files +// By default Teaspoon will look for files that match +// _spec.{js,js.coffee,.coffee}. Add a filename_spec.js file in your spec path +// and it'll be included in the default suite automatically. If you want to +// customize suites, check out the configuration in teaspoon_env.rb +// Manifest +// If you'd rather require your spec files manually (to control order for +// instance) you can disable the suite matcher in the configuration and use this +// file as a manifest. +// For more information: http://github.com/modeset/teaspoon + (function() { diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js index e008ce956ad..7ce3884f844 100644 --- a/spec/javascripts/u2f/authenticate_spec.js +++ b/spec/javascripts/u2f/authenticate_spec.js @@ -1,16 +1,8 @@ /*= require u2f/authenticate */ - - /*= require u2f/util */ - - /*= require u2f/error */ - - /*= require u2f */ - - /*= require ./mock_u2f_device */ (function() { diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js index 21c5266c60e..01d6b7a8961 100644 --- a/spec/javascripts/u2f/register_spec.js +++ b/spec/javascripts/u2f/register_spec.js @@ -1,16 +1,8 @@ /*= require u2f/register */ - - /*= require u2f/util */ - - /*= require u2f/error */ - - /*= require u2f */ - - /*= require ./mock_u2f_device */ (function() { diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js index 3d680ec8ea3..0c1266800d7 100644 --- a/spec/javascripts/zen_mode_spec.js +++ b/spec/javascripts/zen_mode_spec.js @@ -14,8 +14,10 @@ return true; } }; + // Stub Dropzone.forElement(...).enable() }); this.zen = new ZenMode(); + // Set this manually because we can't actually scroll the window return this.zen.scroll_position = 456; }); describe('on enter', function() { @@ -60,7 +62,7 @@ return $('a.js-zen-enter').click(); }; - exitZen = function() { + exitZen = function() { // Ohmmmmmmm return $('a.js-zen-leave').click(); }; diff --git a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb index 593bd6d5cac..e6c90ad87ee 100644 --- a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb @@ -65,14 +65,14 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do expect(reference_filter(act).to_html).to eq exp end - it 'includes a title attribute' do + it 'includes no title attribute' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('title')).to eq range.reference_title + expect(doc.css('a').first.attr('title')).to eq "" end it 'includes default classes' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit_range' + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit_range has-tooltip' end it 'includes a data-project attribute' do diff --git a/spec/lib/banzai/filter/commit_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_reference_filter_spec.rb index d46d3f1489e..e0f08282551 100644 --- a/spec/lib/banzai/filter/commit_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/commit_reference_filter_spec.rb @@ -55,7 +55,7 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do it 'includes a title attribute' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('title')).to eq commit.link_title + expect(doc.css('a').first.attr('title')).to eq commit.title end it 'escapes the title attribute' do @@ -67,7 +67,7 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do it 'includes default classes' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit' + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit has-tooltip' end it 'includes a data-project attribute' do diff --git a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb index 953466679e4..7116c09fb21 100644 --- a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb @@ -64,7 +64,7 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do it 'includes default classes' do doc = filter("Issue #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue' + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue has-tooltip' end it 'supports an :only_path context' do diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb index a005b4990e7..fce86a9b6ad 100644 --- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb @@ -54,7 +54,7 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do it 'includes a title attribute' do doc = reference_filter("Issue #{reference}") - expect(doc.css('a').first.attr('title')).to eq "Issue: #{issue.title}" + expect(doc.css('a').first.attr('title')).to eq issue.title end it 'escapes the title attribute' do @@ -66,7 +66,7 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do it 'includes default classes' do doc = reference_filter("Issue #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue' + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue has-tooltip' end it 'includes a data-project attribute' do diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb index 9276a154007..908ccebbf87 100644 --- a/spec/lib/banzai/filter/label_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb @@ -21,7 +21,7 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do it 'includes default classes' do doc = reference_filter("Label #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-label' + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-label has-tooltip' end it 'includes a data-project attribute' do diff --git a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb index 805acf1c8b3..274258a045c 100644 --- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb @@ -46,7 +46,7 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do it 'includes a title attribute' do doc = reference_filter("Merge #{reference}") - expect(doc.css('a').first.attr('title')).to eq "Merge Request: #{merge.title}" + expect(doc.css('a').first.attr('title')).to eq merge.title end it 'escapes the title attribute' do @@ -58,7 +58,7 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do it 'includes default classes' do doc = reference_filter("Merge #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-merge_request' + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-merge_request has-tooltip' end it 'includes a data-project attribute' do diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb index 9424f2363e1..7419863d848 100644 --- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb @@ -20,7 +20,7 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do it 'includes default classes' do doc = reference_filter("Milestone #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-milestone' + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-milestone has-tooltip' end it 'includes a data-project attribute' do diff --git a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb index 5068ddd7faa..9b92d1a3926 100644 --- a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb @@ -39,7 +39,7 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do it 'includes a title attribute' do doc = reference_filter("Snippet #{reference}") - expect(doc.css('a').first.attr('title')).to eq "Snippet: #{snippet.title}" + expect(doc.css('a').first.attr('title')).to eq snippet.title end it 'escapes the title attribute' do @@ -51,7 +51,7 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do it 'includes default classes' do doc = reference_filter("Snippet #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-snippet' + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-snippet has-tooltip' end it 'includes a data-project attribute' do diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb index 108b36a97cc..fdbdb21eac1 100644 --- a/spec/lib/banzai/filter/user_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb @@ -104,7 +104,7 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do it 'includes default classes' do doc = reference_filter("Hey #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member' + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member has-tooltip' end it 'supports an :only_path context' do diff --git a/spec/lib/banzai/reference_parser/base_parser_spec.rb b/spec/lib/banzai/reference_parser/base_parser_spec.rb index ac9c66e2663..9095d2b1345 100644 --- a/spec/lib/banzai/reference_parser/base_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/base_parser_spec.rb @@ -30,7 +30,7 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do it 'returns the nodes if the attribute value equals the current project ID' do link['data-project'] = project.id.to_s - expect(Ability.abilities).not_to receive(:allowed?) + expect(Ability).not_to receive(:allowed?) expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) end @@ -39,7 +39,7 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do link['data-project'] = other_project.id.to_s - expect(Ability.abilities).to receive(:allowed?). + expect(Ability).to receive(:allowed?). with(user, :read_project, other_project). and_return(true) @@ -57,7 +57,7 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do link['data-project'] = other_project.id.to_s - expect(Ability.abilities).to receive(:allowed?). + expect(Ability).to receive(:allowed?). with(user, :read_project, other_project). and_return(false) @@ -221,7 +221,7 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do it 'delegates the permissions check to the Ability class' do user = double(:user) - expect(Ability.abilities).to receive(:allowed?). + expect(Ability).to receive(:allowed?). with(user, :read_project, project) subject.can?(user, :read_project, project) diff --git a/spec/lib/banzai/reference_parser/user_parser_spec.rb b/spec/lib/banzai/reference_parser/user_parser_spec.rb index 9a82891297d..4e7f82a6e09 100644 --- a/spec/lib/banzai/reference_parser/user_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/user_parser_spec.rb @@ -82,7 +82,7 @@ describe Banzai::ReferenceParser::UserParser, lib: true do end it 'returns the nodes if the user can read the group' do - expect(Ability.abilities).to receive(:allowed?). + expect(Ability).to receive(:allowed?). with(user, :read_group, group). and_return(true) @@ -90,7 +90,7 @@ describe Banzai::ReferenceParser::UserParser, lib: true do end it 'returns an empty Array if the user can not read the group' do - expect(Ability.abilities).to receive(:allowed?). + expect(Ability).to receive(:allowed?). with(user, :read_group, group). and_return(false) @@ -103,7 +103,7 @@ describe Banzai::ReferenceParser::UserParser, lib: true do it 'returns the nodes if the attribute value equals the current project ID' do link['data-project'] = project.id.to_s - expect(Ability.abilities).not_to receive(:allowed?) + expect(Ability).not_to receive(:allowed?) expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) end @@ -113,7 +113,7 @@ describe Banzai::ReferenceParser::UserParser, lib: true do link['data-project'] = other_project.id.to_s - expect(Ability.abilities).to receive(:allowed?). + expect(Ability).to receive(:allowed?). with(user, :read_project, other_project). and_return(true) @@ -125,7 +125,7 @@ describe Banzai::ReferenceParser::UserParser, lib: true do link['data-project'] = other_project.id.to_s - expect(Ability.abilities).to receive(:allowed?). + expect(Ability).to receive(:allowed?). with(user, :read_project, other_project). and_return(false) diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index be51d942af7..af192664b33 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -1250,5 +1250,40 @@ EOT end end end + + describe "#validation_message" do + context "when the YAML could not be parsed" do + it "returns an error about invalid configutaion" do + content = YAML.dump("invalid: yaml: test") + + expect(GitlabCiYamlProcessor.validation_message(content)) + .to eq "Invalid configuration format" + end + end + + context "when the tags parameter is invalid" do + it "returns an error about invalid tags" do + content = YAML.dump({ rspec: { script: "test", tags: "mysql" } }) + + expect(GitlabCiYamlProcessor.validation_message(content)) + .to eq "jobs:rspec tags should be an array of strings" + end + end + + context "when YAML content is empty" do + it "returns an error about missing content" do + expect(GitlabCiYamlProcessor.validation_message('')) + .to eq "Please provide content of .gitlab-ci.yml" + end + end + + context "when the YAML is valid" do + it "does not return any errors" do + content = File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) + + expect(GitlabCiYamlProcessor.validation_message(content)).to be_nil + end + end + end end end diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index b0772cad312..7c23e02d05a 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -7,7 +7,8 @@ describe Gitlab::Auth, lib: true do it 'recognizes CI' do token = '123' project = create(:empty_project) - project.update_attributes(runners_token: token, builds_enabled: true) + project.update_attributes(runners_token: token) + ip = 'ip' expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'gitlab-ci-token') diff --git a/spec/lib/gitlab/ci/config/node/cache_spec.rb b/spec/lib/gitlab/ci/config/node/cache_spec.rb index 50f619ce26e..e251210949c 100644 --- a/spec/lib/gitlab/ci/config/node/cache_spec.rb +++ b/spec/lib/gitlab/ci/config/node/cache_spec.rb @@ -4,7 +4,7 @@ describe Gitlab::Ci::Config::Node::Cache do let(:entry) { described_class.new(config) } describe 'validations' do - before { entry.process! } + before { entry.compose! } context 'when entry config value is correct' do let(:config) do diff --git a/spec/lib/gitlab/ci/config/node/factory_spec.rb b/spec/lib/gitlab/ci/config/node/factory_spec.rb index d26185ba585..a699089c563 100644 --- a/spec/lib/gitlab/ci/config/node/factory_spec.rb +++ b/spec/lib/gitlab/ci/config/node/factory_spec.rb @@ -65,7 +65,8 @@ describe Gitlab::Ci::Config::Node::Factory do .value(nil) .create! - expect(entry).to be_an_instance_of Gitlab::Ci::Config::Node::Undefined + expect(entry) + .to be_an_instance_of Gitlab::Ci::Config::Node::Unspecified end end diff --git a/spec/lib/gitlab/ci/config/node/global_spec.rb b/spec/lib/gitlab/ci/config/node/global_spec.rb index 2f87d270b36..12232ff7e2f 100644 --- a/spec/lib/gitlab/ci/config/node/global_spec.rb +++ b/spec/lib/gitlab/ci/config/node/global_spec.rb @@ -14,7 +14,7 @@ describe Gitlab::Ci::Config::Node::Global do end context 'when hash is valid' do - context 'when all entries defined' do + context 'when some entries defined' do let(:hash) do { before_script: ['ls', 'pwd'], image: 'ruby:2.2', @@ -24,11 +24,11 @@ describe Gitlab::Ci::Config::Node::Global do stages: ['build', 'pages'], cache: { key: 'k', untracked: true, paths: ['public/'] }, rspec: { script: %w[rspec ls] }, - spinach: { script: 'spinach' } } + spinach: { before_script: [], variables: {}, script: 'spinach' } } end - describe '#process!' do - before { global.process! } + describe '#compose!' do + before { global.compose! } it 'creates nodes hash' do expect(global.descendants).to be_an Array @@ -59,7 +59,7 @@ describe Gitlab::Ci::Config::Node::Global do end end - context 'when not processed' do + context 'when not composed' do describe '#before_script' do it 'returns nil' do expect(global.before_script).to be nil @@ -73,8 +73,14 @@ describe Gitlab::Ci::Config::Node::Global do end end - context 'when processed' do - before { global.process! } + context 'when composed' do + before { global.compose! } + + describe '#errors' do + it 'has no errors' do + expect(global.errors).to be_empty + end + end describe '#before_script' do it 'returns correct script' do @@ -137,10 +143,24 @@ describe Gitlab::Ci::Config::Node::Global do expect(global.jobs).to eq( rspec: { name: :rspec, script: %w[rspec ls], - stage: 'test' }, + before_script: ['ls', 'pwd'], + commands: "ls\npwd\nrspec\nls", + image: 'ruby:2.2', + services: ['postgres:9.1', 'mysql:5.5'], + stage: 'test', + cache: { key: 'k', untracked: true, paths: ['public/'] }, + variables: { VAR: 'value' }, + after_script: ['make clean'] }, spinach: { name: :spinach, + before_script: [], script: %w[spinach], - stage: 'test' } + commands: 'spinach', + image: 'ruby:2.2', + services: ['postgres:9.1', 'mysql:5.5'], + stage: 'test', + cache: { key: 'k', untracked: true, paths: ['public/'] }, + variables: {}, + after_script: ['make clean'] }, ) end end @@ -148,17 +168,20 @@ describe Gitlab::Ci::Config::Node::Global do end context 'when most of entires not defined' do - let(:hash) { { cache: { key: 'a' }, rspec: { script: %w[ls] } } } - before { global.process! } + before { global.compose! } + + let(:hash) do + { cache: { key: 'a' }, rspec: { script: %w[ls] } } + end describe '#nodes' do it 'instantizes all nodes' do expect(global.descendants.count).to eq 8 end - it 'contains undefined nodes' do + it 'contains unspecified nodes' do expect(global.descendants.first) - .to be_an_instance_of Gitlab::Ci::Config::Node::Undefined + .to be_an_instance_of Gitlab::Ci::Config::Node::Unspecified end end @@ -188,8 +211,11 @@ describe Gitlab::Ci::Config::Node::Global do # details. # context 'when entires specified but not defined' do - let(:hash) { { variables: nil, rspec: { script: 'rspec' } } } - before { global.process! } + before { global.compose! } + + let(:hash) do + { variables: nil, rspec: { script: 'rspec' } } + end describe '#variables' do it 'undefined entry returns a default value' do @@ -200,7 +226,7 @@ describe Gitlab::Ci::Config::Node::Global do end context 'when hash is not valid' do - before { global.process! } + before { global.compose! } let(:hash) do { before_script: 'ls' } @@ -247,4 +273,27 @@ describe Gitlab::Ci::Config::Node::Global do expect(global.specified?).to be true end end + + describe '#[]' do + before { global.compose! } + + let(:hash) do + { cache: { key: 'a' }, rspec: { script: 'ls' } } + end + + context 'when node exists' do + it 'returns correct entry' do + expect(global[:cache]) + .to be_an_instance_of Gitlab::Ci::Config::Node::Cache + expect(global[:jobs][:rspec][:script].value).to eq ['ls'] + end + end + + context 'when node does not exist' do + it 'always return unspecified node' do + expect(global[:some][:unknown][:node]) + .not_to be_specified + end + end + end end diff --git a/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb b/spec/lib/gitlab/ci/config/node/hidden_spec.rb index cc44e2cc054..61e2a554419 100644 --- a/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb +++ b/spec/lib/gitlab/ci/config/node/hidden_spec.rb @@ -1,15 +1,15 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Node::HiddenJob do +describe Gitlab::Ci::Config::Node::Hidden do let(:entry) { described_class.new(config) } describe 'validations' do context 'when entry config value is correct' do - let(:config) { { image: 'ruby:2.2' } } + let(:config) { [:some, :array] } describe '#value' do it 'returns key value' do - expect(entry.value).to eq(image: 'ruby:2.2') + expect(entry.value).to eq [:some, :array] end end @@ -21,17 +21,6 @@ describe Gitlab::Ci::Config::Node::HiddenJob do end context 'when entry value is not correct' do - context 'incorrect config value type' do - let(:config) { ['incorrect'] } - - describe '#errors' do - it 'saves errors' do - expect(entry.errors) - .to include 'hidden job config should be a hash' - end - end - end - context 'when config is empty' do let(:config) { {} } diff --git a/spec/lib/gitlab/ci/config/node/job_spec.rb b/spec/lib/gitlab/ci/config/node/job_spec.rb index 1484fb60dd8..91f676dae03 100644 --- a/spec/lib/gitlab/ci/config/node/job_spec.rb +++ b/spec/lib/gitlab/ci/config/node/job_spec.rb @@ -3,9 +3,9 @@ require 'spec_helper' describe Gitlab::Ci::Config::Node::Job do let(:entry) { described_class.new(config, name: :rspec) } - before { entry.process! } - describe 'validations' do + before { entry.compose! } + context 'when entry config value is correct' do let(:config) { { script: 'rspec' } } @@ -59,28 +59,82 @@ describe Gitlab::Ci::Config::Node::Job do end end - describe '#value' do - context 'when entry is correct' do + describe '#relevant?' do + it 'is a relevant entry' do + expect(entry).to be_relevant + end + end + + describe '#compose!' do + let(:unspecified) { double('unspecified', 'specified?' => false) } + + let(:specified) do + double('specified', 'specified?' => true, value: 'specified') + end + + let(:deps) { double('deps', '[]' => unspecified) } + + context 'when job config overrides global config' do + before { entry.compose!(deps) } + let(:config) do - { before_script: %w[ls pwd], - script: 'rspec', - after_script: %w[cleanup] } + { image: 'some_image', cache: { key: 'test' } } + end + + it 'overrides global config' do + expect(entry[:image].value).to eq 'some_image' + expect(entry[:cache].value).to eq(key: 'test') + end + end + + context 'when job config does not override global config' do + before do + allow(deps).to receive('[]').with(:image).and_return(specified) + entry.compose!(deps) end - it 'returns correct value' do - expect(entry.value) - .to eq(name: :rspec, - before_script: %w[ls pwd], - script: %w[rspec], - stage: 'test', - after_script: %w[cleanup]) + let(:config) { { script: 'ls', cache: { key: 'test' } } } + + it 'uses config from global entry' do + expect(entry[:image].value).to eq 'specified' + expect(entry[:cache].value).to eq(key: 'test') end end end - describe '#relevant?' do - it 'is a relevant entry' do - expect(entry).to be_relevant + context 'when composed' do + before { entry.compose! } + + describe '#value' do + before { entry.compose! } + + context 'when entry is correct' do + let(:config) do + { before_script: %w[ls pwd], + script: 'rspec', + after_script: %w[cleanup] } + end + + it 'returns correct value' do + expect(entry.value) + .to eq(name: :rspec, + before_script: %w[ls pwd], + script: %w[rspec], + commands: "ls\npwd\nrspec", + stage: 'test', + after_script: %w[cleanup]) + end + end + end + + describe '#commands' do + let(:config) do + { before_script: %w[ls pwd], script: 'rspec' } + end + + it 'returns a string of commands concatenated with new line character' do + expect(entry.commands).to eq "ls\npwd\nrspec" + end end end end diff --git a/spec/lib/gitlab/ci/config/node/jobs_spec.rb b/spec/lib/gitlab/ci/config/node/jobs_spec.rb index b8d9c70479c..929809339ef 100644 --- a/spec/lib/gitlab/ci/config/node/jobs_spec.rb +++ b/spec/lib/gitlab/ci/config/node/jobs_spec.rb @@ -4,7 +4,7 @@ describe Gitlab::Ci::Config::Node::Jobs do let(:entry) { described_class.new(config) } describe 'validations' do - before { entry.process! } + before { entry.compose! } context 'when entry config value is correct' do let(:config) { { rspec: { script: 'rspec' } } } @@ -47,8 +47,8 @@ describe Gitlab::Ci::Config::Node::Jobs do end end - context 'when valid job entries processed' do - before { entry.process! } + context 'when valid job entries composed' do + before { entry.compose! } let(:config) do { rspec: { script: 'rspec' }, @@ -61,9 +61,11 @@ describe Gitlab::Ci::Config::Node::Jobs do expect(entry.value).to eq( rspec: { name: :rspec, script: %w[rspec], + commands: 'rspec', stage: 'test' }, spinach: { name: :spinach, script: %w[spinach], + commands: 'spinach', stage: 'test' }) end end @@ -74,7 +76,7 @@ describe Gitlab::Ci::Config::Node::Jobs do expect(entry.descendants.first(2)) .to all(be_an_instance_of(Gitlab::Ci::Config::Node::Job)) expect(entry.descendants.last) - .to be_an_instance_of(Gitlab::Ci::Config::Node::HiddenJob) + .to be_an_instance_of(Gitlab::Ci::Config::Node::Hidden) end end diff --git a/spec/lib/gitlab/ci/config/node/null_spec.rb b/spec/lib/gitlab/ci/config/node/null_spec.rb deleted file mode 100644 index 1ab5478dcfa..00000000000 --- a/spec/lib/gitlab/ci/config/node/null_spec.rb +++ /dev/null @@ -1,41 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Ci::Config::Node::Null do - let(:null) { described_class.new(nil) } - - describe '#leaf?' do - it 'is leaf node' do - expect(null).to be_leaf - end - end - - describe '#valid?' do - it 'is always valid' do - expect(null).to be_valid - end - end - - describe '#errors' do - it 'is does not contain errors' do - expect(null.errors).to be_empty - end - end - - describe '#value' do - it 'returns nil' do - expect(null.value).to eq nil - end - end - - describe '#relevant?' do - it 'is not relevant' do - expect(null.relevant?).to eq false - end - end - - describe '#specified?' do - it 'is not defined' do - expect(null.specified?).to eq false - end - end -end diff --git a/spec/lib/gitlab/ci/config/node/script_spec.rb b/spec/lib/gitlab/ci/config/node/script_spec.rb index ee7395362a9..219a7e981d3 100644 --- a/spec/lib/gitlab/ci/config/node/script_spec.rb +++ b/spec/lib/gitlab/ci/config/node/script_spec.rb @@ -3,9 +3,7 @@ require 'spec_helper' describe Gitlab::Ci::Config::Node::Script do let(:entry) { described_class.new(config) } - describe '#process!' do - before { entry.process! } - + describe 'validations' do context 'when entry config value is correct' do let(:config) { ['ls', 'pwd'] } diff --git a/spec/lib/gitlab/ci/config/node/undefined_spec.rb b/spec/lib/gitlab/ci/config/node/undefined_spec.rb index 2d43e1c1a9d..6bde8602963 100644 --- a/spec/lib/gitlab/ci/config/node/undefined_spec.rb +++ b/spec/lib/gitlab/ci/config/node/undefined_spec.rb @@ -1,32 +1,41 @@ require 'spec_helper' describe Gitlab::Ci::Config::Node::Undefined do - let(:undefined) { described_class.new(entry) } - let(:entry) { spy('Entry') } + let(:entry) { described_class.new } + + describe '#leaf?' do + it 'is leaf node' do + expect(entry).to be_leaf + end + end describe '#valid?' do - it 'delegates method to entry' do - expect(undefined.valid).to eq entry + it 'is always valid' do + expect(entry).to be_valid end end describe '#errors' do - it 'delegates method to entry' do - expect(undefined.errors).to eq entry + it 'is does not contain errors' do + expect(entry.errors).to be_empty end end describe '#value' do - it 'delegates method to entry' do - expect(undefined.value).to eq entry + it 'returns nil' do + expect(entry.value).to eq nil end end - describe '#specified?' do - it 'is always false' do - allow(entry).to receive(:specified?).and_return(true) + describe '#relevant?' do + it 'is not relevant' do + expect(entry.relevant?).to eq false + end + end - expect(undefined.specified?).to be false + describe '#specified?' do + it 'is not defined' do + expect(entry.specified?).to eq false end end end diff --git a/spec/lib/gitlab/ci/config/node/unspecified_spec.rb b/spec/lib/gitlab/ci/config/node/unspecified_spec.rb new file mode 100644 index 00000000000..ba3ceef24ce --- /dev/null +++ b/spec/lib/gitlab/ci/config/node/unspecified_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Node::Unspecified do + let(:unspecified) { described_class.new(entry) } + let(:entry) { spy('Entry') } + + describe '#valid?' do + it 'delegates method to entry' do + expect(unspecified.valid?).to eq entry + end + end + + describe '#errors' do + it 'delegates method to entry' do + expect(unspecified.errors).to eq entry + end + end + + describe '#value' do + it 'delegates method to entry' do + expect(unspecified.value).to eq entry + end + end + + describe '#specified?' do + it 'is always false' do + allow(entry).to receive(:specified?).and_return(true) + + expect(unspecified.specified?).to be false + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline_duration_spec.rb b/spec/lib/gitlab/ci/pipeline_duration_spec.rb new file mode 100644 index 00000000000..b26728a843c --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline_duration_spec.rb @@ -0,0 +1,115 @@ +require 'spec_helper' + +describe Gitlab::Ci::PipelineDuration do + let(:calculated_duration) { calculate(data) } + + shared_examples 'calculating duration' do + it do + expect(calculated_duration).to eq(duration) + end + end + + context 'test sample A' do + let(:data) do + [[0, 1], + [1, 2], + [3, 4], + [5, 6]] + end + + let(:duration) { 4 } + + it_behaves_like 'calculating duration' + end + + context 'test sample B' do + let(:data) do + [[0, 1], + [1, 2], + [2, 3], + [3, 4], + [0, 4]] + end + + let(:duration) { 4 } + + it_behaves_like 'calculating duration' + end + + context 'test sample C' do + let(:data) do + [[0, 4], + [2, 6], + [5, 7], + [8, 9]] + end + + let(:duration) { 8 } + + it_behaves_like 'calculating duration' + end + + context 'test sample D' do + let(:data) do + [[0, 1], + [2, 3], + [4, 5], + [6, 7]] + end + + let(:duration) { 4 } + + it_behaves_like 'calculating duration' + end + + context 'test sample E' do + let(:data) do + [[0, 1], + [3, 9], + [3, 4], + [3, 5], + [3, 8], + [4, 5], + [4, 7], + [5, 8]] + end + + let(:duration) { 7 } + + it_behaves_like 'calculating duration' + end + + context 'test sample F' do + let(:data) do + [[1, 3], + [2, 4], + [2, 4], + [2, 4], + [5, 8]] + end + + let(:duration) { 6 } + + it_behaves_like 'calculating duration' + end + + context 'test sample G' do + let(:data) do + [[1, 3], + [2, 4], + [6, 7]] + end + + let(:duration) { 4 } + + it_behaves_like 'calculating duration' + end + + def calculate(data) + periods = data.shuffle.map do |(first, last)| + Gitlab::Ci::PipelineDuration::Period.new(first, last) + end + + Gitlab::Ci::PipelineDuration.from_periods(periods.sort_by(&:first)) + end +end diff --git a/spec/lib/gitlab/conflict/parser_spec.rb b/spec/lib/gitlab/conflict/parser_spec.rb index 65a828accde..16eb3766356 100644 --- a/spec/lib/gitlab/conflict/parser_spec.rb +++ b/spec/lib/gitlab/conflict/parser_spec.rb @@ -179,10 +179,15 @@ CONFLICT to raise_error(Gitlab::Conflict::Parser::UnmergeableFile) end - it 'raises UnmergeableFile when the file is over 100 KB' do - expect { parse_text('a' * 102401) }. + it 'raises UnmergeableFile when the file is over 200 KB' do + expect { parse_text('a' * 204801) }. to raise_error(Gitlab::Conflict::Parser::UnmergeableFile) end + + it 'raises UnsupportedEncoding when the file contains non-UTF-8 characters' do + expect { parse_text("a\xC4\xFC".force_encoding(Encoding::ASCII_8BIT)) }. + to raise_error(Gitlab::Conflict::Parser::UnsupportedEncoding) + end end end end diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb index b7c3bc4e1a7..7df288f619f 100644 --- a/spec/lib/gitlab/github_import/importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Gitlab::GithubImport::Importer, lib: true do describe '#execute' do context 'when an error occurs' do - let(:project) { create(:project, import_url: 'https://github.com/octocat/Hello-World.git', wiki_enabled: false) } + let(:project) { create(:project, import_url: 'https://github.com/octocat/Hello-World.git', wiki_access_level: ProjectFeature::DISABLED) } let(:octocat) { double(id: 123456, login: 'octocat') } let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } @@ -13,7 +13,7 @@ describe Gitlab::GithubImport::Importer, lib: true do let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id } let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha) } - let(:label) do + let(:label1) do double( name: 'Bug', color: 'ff0000', @@ -21,6 +21,14 @@ describe Gitlab::GithubImport::Importer, lib: true do ) end + let(:label2) do + double( + name: nil, + color: 'ff0000', + url: 'https://api.github.com/repos/octocat/Hello-World/labels/bug' + ) + end + let(:milestone) do double( number: 1347, @@ -93,7 +101,7 @@ describe Gitlab::GithubImport::Importer, lib: true do before do allow(project).to receive(:import_data).and_return(double.as_null_object) allow_any_instance_of(Octokit::Client).to receive(:rate_limit!).and_raise(Octokit::NotFound) - allow_any_instance_of(Octokit::Client).to receive(:labels).and_return([label, label]) + allow_any_instance_of(Octokit::Client).to receive(:labels).and_return([label1, label2]) allow_any_instance_of(Octokit::Client).to receive(:milestones).and_return([milestone, milestone]) allow_any_instance_of(Octokit::Client).to receive(:issues).and_return([issue1, issue2]) allow_any_instance_of(Octokit::Client).to receive(:pull_requests).and_return([pull_request, pull_request]) @@ -113,7 +121,7 @@ describe Gitlab::GithubImport::Importer, lib: true do error = { message: 'The remote data could not be fully imported.', errors: [ - { type: :label, url: "https://api.github.com/repos/octocat/Hello-World/labels/bug", errors: "Validation failed: Title has already been taken" }, + { type: :label, url: "https://api.github.com/repos/octocat/Hello-World/labels/bug", errors: "Validation failed: Title can't be blank, Title is invalid" }, { type: :milestone, url: "https://api.github.com/repos/octocat/Hello-World/milestones/1", errors: "Validation failed: Title has already been taken" }, { type: :issue, url: "https://api.github.com/repos/octocat/Hello-World/issues/1347", errors: "Invalid Repository. Use user/repo format." }, { type: :issue, url: "https://api.github.com/repos/octocat/Hello-World/issues/1348", errors: "Validation failed: Title can't be blank, Title is too short (minimum is 0 characters)" }, diff --git a/spec/lib/gitlab/github_import/issue_formatter_spec.rb b/spec/lib/gitlab/github_import/issue_formatter_spec.rb index 0e7ffbe9b8e..d60c4111e99 100644 --- a/spec/lib/gitlab/github_import/issue_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/issue_formatter_spec.rb @@ -48,8 +48,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do end context 'when issue is closed' do - let(:closed_at) { DateTime.strptime('2011-01-28T19:01:12Z') } - let(:raw_data) { double(base_data.merge(state: 'closed', closed_at: closed_at)) } + let(:raw_data) { double(base_data.merge(state: 'closed')) } it 'returns formatted attributes' do expected = { @@ -62,7 +61,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do author_id: project.creator_id, assignee_id: nil, created_at: created_at, - updated_at: closed_at + updated_at: updated_at } expect(issue.attributes).to eq(expected) diff --git a/spec/lib/gitlab/github_import/label_formatter_spec.rb b/spec/lib/gitlab/github_import/label_formatter_spec.rb index 87593e32db0..8098754d735 100644 --- a/spec/lib/gitlab/github_import/label_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/label_formatter_spec.rb @@ -1,18 +1,34 @@ require 'spec_helper' describe Gitlab::GithubImport::LabelFormatter, lib: true do - describe '#attributes' do - it 'returns formatted attributes' do - project = create(:project) - raw = double(name: 'improvements', color: 'e6e6e6') + let(:project) { create(:project) } + let(:raw) { double(name: 'improvements', color: 'e6e6e6') } - formatter = described_class.new(project, raw) + subject { described_class.new(project, raw) } - expect(formatter.attributes).to eq({ + describe '#attributes' do + it 'returns formatted attributes' do + expect(subject.attributes).to eq({ project: project, title: 'improvements', color: '#e6e6e6' }) end end + + describe '#create!' do + context 'when label does not exist' do + it 'creates a new label' do + expect { subject.create! }.to change(Label, :count).by(1) + end + end + + context 'when label exists' do + it 'does not create a new label' do + project.labels.create(name: raw.name) + + expect { subject.create! }.not_to change(Label, :count) + end + end + end end diff --git a/spec/lib/gitlab/github_import/milestone_formatter_spec.rb b/spec/lib/gitlab/github_import/milestone_formatter_spec.rb index 5a421e50581..09337c99a07 100644 --- a/spec/lib/gitlab/github_import/milestone_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/milestone_formatter_spec.rb @@ -40,8 +40,7 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do end context 'when milestone is closed' do - let(:closed_at) { DateTime.strptime('2011-01-28T19:01:12Z') } - let(:raw_data) { double(base_data.merge(state: 'closed', closed_at: closed_at)) } + let(:raw_data) { double(base_data.merge(state: 'closed')) } it 'returns formatted attributes' do expected = { @@ -52,7 +51,7 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do state: 'closed', due_date: nil, created_at: created_at, - updated_at: closed_at + updated_at: updated_at } expect(formatter.attributes).to eq(expected) diff --git a/spec/lib/gitlab/github_import/project_creator_spec.rb b/spec/lib/gitlab/github_import/project_creator_spec.rb index 0f363b8b0aa..014ee462e5c 100644 --- a/spec/lib/gitlab/github_import/project_creator_spec.rb +++ b/spec/lib/gitlab/github_import/project_creator_spec.rb @@ -2,33 +2,59 @@ require 'spec_helper' describe Gitlab::GithubImport::ProjectCreator, lib: true do let(:user) { create(:user) } + let(:namespace) { create(:group, owner: user) } + let(:repo) do OpenStruct.new( login: 'vim', name: 'vim', - private: true, full_name: 'asd/vim', - clone_url: "https://gitlab.com/asd/vim.git", - owner: OpenStruct.new(login: "john") + clone_url: 'https://gitlab.com/asd/vim.git' ) end - let(:namespace) { create(:group, owner: user) } - let(:token) { "asdffg" } - let(:access_params) { { github_access_token: token } } + + subject(:service) { described_class.new(repo, namespace, user, github_access_token: 'asdffg') } before do namespace.add_owner(user) + allow_any_instance_of(Project).to receive(:add_import_job) end - it 'creates project' do - allow_any_instance_of(Project).to receive(:add_import_job) + describe '#execute' do + it 'creates a project' do + expect { service.execute }.to change(Project, :count).by(1) + end + + it 'handle GitHub credentials' do + project = service.execute + + expect(project.import_url).to eq('https://asdffg@gitlab.com/asd/vim.git') + expect(project.safe_import_url).to eq('https://*****@gitlab.com/asd/vim.git') + expect(project.import_data.credentials).to eq(user: 'asdffg', password: nil) + end + + context 'when Github project is private' do + it 'sets project visibility to private' do + repo.private = true + + project = service.execute + + expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) + end + end + + context 'when Github project is public' do + before do + allow_any_instance_of(ApplicationSetting).to receive(:default_project_visibility).and_return(Gitlab::VisibilityLevel::INTERNAL) + end + + it 'sets project visibility to the default project visibility' do + repo.private = false - project_creator = Gitlab::GithubImport::ProjectCreator.new(repo, namespace, user, access_params) - project = project_creator.execute + project = service.execute - expect(project.import_url).to eq("https://asdffg@gitlab.com/asd/vim.git") - expect(project.safe_import_url).to eq("https://*****@gitlab.com/asd/vim.git") - expect(project.import_data.credentials).to eq(user: "asdffg", password: nil) - expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) + expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL) + end + end end end diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index b667abf063d..edfc6ad81c6 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -62,8 +62,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end context 'when pull request is closed' do - let(:closed_at) { DateTime.strptime('2011-01-28T19:01:12Z') } - let(:raw_data) { double(base_data.merge(state: 'closed', closed_at: closed_at)) } + let(:raw_data) { double(base_data.merge(state: 'closed')) } it 'returns formatted attributes' do expected = { @@ -81,7 +80,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do author_id: project.creator_id, assignee_id: nil, created_at: created_at, - updated_at: closed_at + updated_at: updated_at } expect(pull_request.attributes).to eq(expected) @@ -108,7 +107,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do author_id: project.creator_id, assignee_id: nil, created_at: created_at, - updated_at: merged_at + updated_at: updated_at } expect(pull_request.attributes).to eq(expected) diff --git a/spec/lib/gitlab/gitlab_import/importer_spec.rb b/spec/lib/gitlab/gitlab_import/importer_spec.rb index d3f1deb3837..9b499b593d3 100644 --- a/spec/lib/gitlab/gitlab_import/importer_spec.rb +++ b/spec/lib/gitlab/gitlab_import/importer_spec.rb @@ -13,6 +13,7 @@ describe Gitlab::GitlabImport::Importer, lib: true do 'title' => 'Issue', 'description' => 'Lorem ipsum', 'state' => 'opened', + 'confidential' => true, 'author' => { 'id' => 283999, 'name' => 'John Doe' @@ -34,6 +35,7 @@ describe Gitlab::GitlabImport::Importer, lib: true do title: 'Issue', description: "*Created by: John Doe*\n\nLorem ipsum", state: 'opened', + confidential: true, author_id: project.creator_id } diff --git a/spec/lib/gitlab/gitorious_import/project_creator_spec.rb b/spec/lib/gitlab/gitorious_import/project_creator_spec.rb deleted file mode 100644 index 946712ca38e..00000000000 --- a/spec/lib/gitlab/gitorious_import/project_creator_spec.rb +++ /dev/null @@ -1,26 +0,0 @@ -require 'spec_helper' - -describe Gitlab::GitoriousImport::ProjectCreator, lib: true do - let(:user) { create(:user) } - let(:repo) { Gitlab::GitoriousImport::Repository.new('foo/bar-baz-qux') } - let(:namespace){ create(:group, owner: user) } - - before do - namespace.add_owner(user) - end - - it 'creates project' do - allow_any_instance_of(Project).to receive(:add_import_job) - - project_creator = Gitlab::GitoriousImport::ProjectCreator.new(repo, namespace, user) - project = project_creator.execute - - expect(project.name).to eq("Bar Baz Qux") - expect(project.path).to eq("bar-baz-qux") - expect(project.namespace).to eq(namespace) - expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) - expect(project.import_type).to eq("gitorious") - expect(project.import_source).to eq("foo/bar-baz-qux") - expect(project.import_url).to eq("https://gitorious.org/foo/bar-baz-qux.git") - end -end diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index cbbf98dca94..5114f9c55e1 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -1,9 +1,5 @@ { "description": "Nisi et repellendus ut enim quo accusamus vel magnam.", - "issues_enabled": true, - "merge_requests_enabled": true, - "wiki_enabled": true, - "snippets_enabled": false, "visibility_level": 10, "archived": false, "issues": [ @@ -7307,4 +7303,4 @@ "protected_branches": [ ] -}
\ No newline at end of file +} diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index 4d857945fde..a07ef279e68 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do let(:user) { create(:user) } let(:namespace) { create(:namespace, owner: user) } let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') } - let(:project) { create(:empty_project, name: 'project', path: 'project') } + let!(:project) { create(:empty_project, name: 'project', path: 'project', builds_access_level: ProjectFeature::DISABLED, issues_access_level: ProjectFeature::DISABLED) } let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) } let(:restored_project_json) { project_tree_restorer.restore } @@ -18,6 +18,17 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do expect(restored_project_json).to be true end + it 'restore correct project features' do + restored_project_json + project = Project.find_by_path('project') + + expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED) + expect(project.project_feature.builds_access_level).to eq(ProjectFeature::DISABLED) + expect(project.project_feature.snippets_access_level).to eq(ProjectFeature::ENABLED) + expect(project.project_feature.wiki_access_level).to eq(ProjectFeature::ENABLED) + expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::ENABLED) + end + it 'creates a valid pipeline note' do restored_project_json diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index 3a86a4ce07c..d891c2d0cc6 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -111,6 +111,14 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do expect(saved_project_json['issues'].first['label_links'].first['label']).not_to be_empty end + it 'has project feature' do + project_feature = saved_project_json['project_feature'] + expect(project_feature).not_to be_empty + expect(project_feature["issues_access_level"]).to eq(ProjectFeature::DISABLED) + expect(project_feature["wiki_access_level"]).to eq(ProjectFeature::ENABLED) + expect(project_feature["builds_access_level"]).to eq(ProjectFeature::PRIVATE) + end + it 'does not complain about non UTF-8 characters in MR diffs' do ActiveRecord::Base.connection.execute("UPDATE merge_request_diffs SET st_diffs = '---\n- :diff: !binary |-\n LS0tIC9kZXYvbnVsbAorKysgYi9pbWFnZXMvbnVjb3IucGRmCkBAIC0wLDAg\n KzEsMTY3OSBAQAorJVBERi0xLjUNJeLjz9MNCisxIDAgb2JqDTw8L01ldGFk\n YXR'") @@ -154,6 +162,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do create(:event, target: milestone, project: project, action: Event::CREATED, author: user) + project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) + project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::ENABLED) + project.project_feature.update_attribute(:builds_access_level, ProjectFeature::PRIVATE) + project end diff --git a/spec/lib/gitlab/import_export/reader_spec.rb b/spec/lib/gitlab/import_export/reader_spec.rb index b6dec41d218..3ceb1e7e803 100644 --- a/spec/lib/gitlab/import_export/reader_spec.rb +++ b/spec/lib/gitlab/import_export/reader_spec.rb @@ -32,6 +32,12 @@ describe Gitlab::ImportExport::Reader, lib: true do expect(described_class.new(shared: shared).project_tree).to match(include: [:issues]) end + it 'generates the correct hash for a single project feature relation' do + setup_yaml(project_tree: [:project_feature]) + + expect(described_class.new(shared: shared).project_tree).to match(include: [:project_feature]) + end + it 'generates the correct hash for a multiple project relation' do setup_yaml(project_tree: [:issues, :snippets]) diff --git a/spec/lib/gitlab/ldap/adapter_spec.rb b/spec/lib/gitlab/ldap/adapter_spec.rb index 4847b5f3b0e..0600893f4cf 100644 --- a/spec/lib/gitlab/ldap/adapter_spec.rb +++ b/spec/lib/gitlab/ldap/adapter_spec.rb @@ -1,12 +1,77 @@ require 'spec_helper' describe Gitlab::LDAP::Adapter, lib: true do - let(:adapter) { Gitlab::LDAP::Adapter.new 'ldapmain' } + include LdapHelpers + + let(:ldap) { double(:ldap) } + let(:adapter) { ldap_adapter('ldapmain', ldap) } + + describe '#users' do + before do + stub_ldap_config(base: 'dc=example,dc=com') + end + + it 'searches with the proper options when searching by uid' do + # Requires this expectation style to match the filter + expect(adapter).to receive(:ldap_search) do |arg| + expect(arg[:filter].to_s).to eq('(uid=johndoe)') + expect(arg[:base]).to eq('dc=example,dc=com') + expect(arg[:attributes]).to match(%w{uid cn mail dn}) + end.and_return({}) + + adapter.users('uid', 'johndoe') + end + + it 'searches with the proper options when searching by dn' do + expect(adapter).to receive(:ldap_search).with( + base: 'uid=johndoe,ou=users,dc=example,dc=com', + scope: Net::LDAP::SearchScope_BaseObject, + attributes: %w{uid cn mail dn}, + filter: nil + ).and_return({}) + + adapter.users('dn', 'uid=johndoe,ou=users,dc=example,dc=com') + end + + it 'searches with the proper options when searching with a limit' do + expect(adapter) + .to receive(:ldap_search).with(hash_including(size: 100)).and_return({}) + + adapter.users('uid', 'johndoe', 100) + end + + it 'returns an LDAP::Person if search returns a result' do + entry = ldap_user_entry('johndoe') + allow(adapter).to receive(:ldap_search).and_return([entry]) + + results = adapter.users('uid', 'johndoe') + + expect(results.size).to eq(1) + expect(results.first.uid).to eq('johndoe') + end + + it 'returns empty array if search entry does not respond to uid' do + entry = Net::LDAP::Entry.new + entry['dn'] = user_dn('johndoe') + allow(adapter).to receive(:ldap_search).and_return([entry]) + + results = adapter.users('uid', 'johndoe') + + expect(results).to be_empty + end + + it 'uses the right uid attribute when non-default' do + stub_ldap_config(uid: 'sAMAccountName') + expect(adapter).to receive(:ldap_search).with( + hash_including(attributes: %w{sAMAccountName cn mail dn}) + ).and_return({}) + + adapter.users('sAMAccountName', 'johndoe') + end + end describe '#dn_matches_filter?' do - let(:ldap) { double(:ldap) } subject { adapter.dn_matches_filter?(:dn, :filter) } - before { allow(adapter).to receive(:ldap).and_return(ldap) } context "when the search is successful" do context "and the result is non-empty" do diff --git a/spec/lib/gitlab/metrics/rack_middleware_spec.rb b/spec/lib/gitlab/metrics/rack_middleware_spec.rb index a30cb2a5e38..bcaffd27909 100644 --- a/spec/lib/gitlab/metrics/rack_middleware_spec.rb +++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb @@ -19,7 +19,7 @@ describe Gitlab::Metrics::RackMiddleware do end it 'tags a transaction with the name and action of a controller' do - klass = double(:klass, name: 'TestController') + klass = double(:klass, name: 'TestController', content_type: 'text/html') controller = double(:controller, class: klass, action_name: 'show') env['action_controller.instance'] = controller @@ -32,7 +32,7 @@ describe Gitlab::Metrics::RackMiddleware do middleware.call(env) end - it 'tags a transaction with the method andpath of the route in the grape endpoint' do + it 'tags a transaction with the method and path of the route in the grape endpoint' do route = double(:route, route_method: "GET", route_path: "/:version/projects/:id/archive(.:format)") endpoint = double(:endpoint, route: route) @@ -87,17 +87,30 @@ describe Gitlab::Metrics::RackMiddleware do describe '#tag_controller' do let(:transaction) { middleware.transaction_from_env(env) } + let(:content_type) { 'text/html' } - it 'tags a transaction with the name and action of a controller' do + before do klass = double(:klass, name: 'TestController') - controller = double(:controller, class: klass, action_name: 'show') + controller = double(:controller, class: klass, action_name: 'show', content_type: content_type) env['action_controller.instance'] = controller + end + it 'tags a transaction with the name and action of a controller' do middleware.tag_controller(transaction, env) expect(transaction.action).to eq('TestController#show') end + + context 'when the response content type is not :html' do + let(:content_type) { 'application/json' } + + it 'appends the mime type to the transaction action' do + middleware.tag_controller(transaction, env) + + expect(transaction.action).to eq('TestController#show.json') + end + end end describe '#tag_endpoint' do diff --git a/spec/lib/gitlab/popen_spec.rb b/spec/lib/gitlab/popen_spec.rb index e8b236426e9..4ae216d55b0 100644 --- a/spec/lib/gitlab/popen_spec.rb +++ b/spec/lib/gitlab/popen_spec.rb @@ -40,4 +40,13 @@ describe 'Gitlab::Popen', lib: true, no_db: true do it { expect(@status).to be_zero } it { expect(@output).to include('spec') } end + + context 'use stdin' do + before do + @output, @status = @klass.new.popen(%w[cat]) { |stdin| stdin.write 'hello' } + end + + it { expect(@status).to be_zero } + it { expect(@output).to eq('hello') } + end end diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb index 8a656ab0ee9..dfbefad6367 100644 --- a/spec/lib/gitlab/search_results_spec.rb +++ b/spec/lib/gitlab/search_results_spec.rb @@ -12,12 +12,6 @@ describe Gitlab::SearchResults do let!(:milestone) { create(:milestone, project: project, title: 'foo') } let(:results) { described_class.new(user, Project.all, 'foo') } - describe '#total_count' do - it 'returns the total amount of search hits' do - expect(results.total_count).to eq(4) - end - end - describe '#projects_count' do it 'returns the total amount of projects' do expect(results.projects_count).to eq(1) @@ -42,18 +36,6 @@ describe Gitlab::SearchResults do end end - describe '#empty?' do - it 'returns true when there are no search results' do - allow(results).to receive(:total_count).and_return(0) - - expect(results.empty?).to eq(true) - end - - it 'returns false when there are search results' do - expect(results.empty?).to eq(false) - end - end - describe 'confidential issues' do let(:project_1) { create(:empty_project) } let(:project_2) { create(:empty_project) } diff --git a/spec/lib/gitlab/snippet_search_results_spec.rb b/spec/lib/gitlab/snippet_search_results_spec.rb index e86b9ef6a63..b661a894c0c 100644 --- a/spec/lib/gitlab/snippet_search_results_spec.rb +++ b/spec/lib/gitlab/snippet_search_results_spec.rb @@ -5,12 +5,6 @@ describe Gitlab::SnippetSearchResults do let(:results) { described_class.new(Snippet.all, 'foo') } - describe '#total_count' do - it 'returns the total amount of search hits' do - expect(results.total_count).to eq(2) - end - end - describe '#snippet_titles_count' do it 'returns the amount of matched snippet titles' do expect(results.snippet_titles_count).to eq(1) diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index c5c1402e8fc..395192149a9 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -4,7 +4,7 @@ describe Gitlab::Workhorse, lib: true do let(:project) { create(:project) } let(:subject) { Gitlab::Workhorse } - describe "#send_git_archive" do + describe ".send_git_archive" do context "when the repository doesn't have an archive file path" do before do allow(project.repository).to receive(:archive_metadata).and_return(Hash.new) @@ -15,4 +15,88 @@ describe Gitlab::Workhorse, lib: true do end end end + + describe ".secret" do + subject { described_class.secret } + + before do + described_class.instance_variable_set(:@secret, nil) + described_class.write_secret + end + + it 'returns 32 bytes' do + expect(subject).to be_a(String) + expect(subject.length).to eq(32) + expect(subject.encoding).to eq(Encoding::ASCII_8BIT) + end + + it 'raises an exception if the secret file cannot be read' do + File.delete(described_class.secret_path) + expect { subject }.to raise_exception(Errno::ENOENT) + end + + it 'raises an exception if the secret file contains the wrong number of bytes' do + File.truncate(described_class.secret_path, 0) + expect { subject }.to raise_exception(RuntimeError) + end + end + + describe ".write_secret" do + let(:secret_path) { described_class.secret_path } + before do + begin + File.delete(secret_path) + rescue Errno::ENOENT + end + + described_class.write_secret + end + + it 'uses mode 0600' do + expect(File.stat(secret_path).mode & 0777).to eq(0600) + end + + it 'writes base64 data' do + bytes = Base64.strict_decode64(File.read(secret_path)) + expect(bytes).not_to be_empty + end + end + + describe '#verify_api_request!' do + let(:header_key) { described_class::INTERNAL_API_REQUEST_HEADER } + let(:payload) { { 'iss' => 'gitlab-workhorse' } } + + it 'accepts a correct header' do + headers = { header_key => JWT.encode(payload, described_class.secret, 'HS256') } + expect { call_verify(headers) }.not_to raise_error + end + + it 'raises an error when the header is not set' do + expect { call_verify({}) }.to raise_jwt_error + end + + it 'raises an error when the header is not signed' do + headers = { header_key => JWT.encode(payload, nil, 'none') } + expect { call_verify(headers) }.to raise_jwt_error + end + + it 'raises an error when the header is signed with the wrong key' do + headers = { header_key => JWT.encode(payload, 'wrongkey', 'HS256') } + expect { call_verify(headers) }.to raise_jwt_error + end + + it 'raises an error when the issuer is incorrect' do + payload['iss'] = 'somebody else' + headers = { header_key => JWT.encode(payload, described_class.secret, 'HS256') } + expect { call_verify(headers) }.to raise_jwt_error + end + + def raise_jwt_error + raise_error(JWT::DecodeError) + end + + def call_verify(headers) + described_class.verify_api_request!(headers) + end + end end diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index aa3b2bbf471..1bdf005c823 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -171,70 +171,6 @@ describe Ability, lib: true do end end - shared_examples_for ".project_abilities" do |enable_request_store| - before do - RequestStore.begin! if enable_request_store - end - - after do - if enable_request_store - RequestStore.end! - RequestStore.clear! - end - end - - describe '.project_abilities' do - let!(:project) { create(:empty_project, :public) } - let!(:user) { create(:user) } - - it 'returns permissions for admin user' do - admin = create(:admin) - - results = described_class.project_abilities(admin, project) - - expect(results.count).to eq(68) - end - - it 'returns permissions for an owner' do - results = described_class.project_abilities(project.owner, project) - - expect(results.count).to eq(68) - end - - it 'returns permissions for a master' do - project.team << [user, :master] - - results = described_class.project_abilities(user, project) - - expect(results.count).to eq(60) - end - - it 'returns permissions for a developer' do - project.team << [user, :developer] - - results = described_class.project_abilities(user, project) - - expect(results.count).to eq(44) - end - - it 'returns permissions for a guest' do - project.team << [user, :guest] - - results = described_class.project_abilities(user, project) - - expect(results.count).to eq(21) - end - end - end - - describe '.project_abilities with RequestStore' do - it_behaves_like ".project_abilities", true - end - - describe '.project_abilities without RequestStore' do - it_behaves_like ".project_abilities", false - end - describe '.issues_readable_by_user' do context 'with an admin user' do it 'returns all given issues' do @@ -282,4 +218,17 @@ describe Ability, lib: true do end end end + + describe '.project_disabled_features_rules' do + let(:project) { create(:project, wiki_access_level: ProjectFeature::DISABLED) } + + subject { described_class.allowed(project.owner, project) } + + context 'wiki named abilities' do + it 'disables wiki abilities if the project has no wiki' do + expect(project).to receive(:has_external_wiki?).and_return(false) + expect(subject).not_to include(:read_wiki, :create_wiki, :update_wiki, :admin_wiki) + end + end + end end diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index ee2c3d04984..c45c2635cf4 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -948,15 +948,17 @@ describe Ci::Build, models: true do before { build.run! } it 'returns false' do - expect(build.retryable?).to be false + expect(build).not_to be_retryable end end context 'when build is finished' do - before { build.success! } + before do + build.success! + end it 'returns true' do - expect(build.retryable?).to be true + expect(build).to be_retryable end end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 36d10636ae9..bce18b4e99e 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -19,4 +19,64 @@ describe Ci::Build, models: true do expect(build.trace).to eq(test_trace) end end + + describe '#has_trace_file?' do + context 'when there is no trace' do + it { expect(build.has_trace_file?).to be_falsey } + it { expect(build.trace).to be_nil } + end + + context 'when there is a trace' do + context 'when trace is stored in file' do + let(:build_with_trace) { create(:ci_build, :trace) } + + it { expect(build_with_trace.has_trace_file?).to be_truthy } + it { expect(build_with_trace.trace).to eq('BUILD TRACE') } + end + + context 'when trace is stored in old file' do + before do + allow(build.project).to receive(:ci_id).and_return(999) + allow(File).to receive(:exist?).with(build.path_to_trace).and_return(false) + allow(File).to receive(:exist?).with(build.old_path_to_trace).and_return(true) + allow(File).to receive(:read).with(build.old_path_to_trace).and_return(test_trace) + end + + it { expect(build.has_trace_file?).to be_truthy } + it { expect(build.trace).to eq(test_trace) } + end + + context 'when trace is stored in DB' do + before do + allow(build.project).to receive(:ci_id).and_return(nil) + allow(build).to receive(:read_attribute).with(:trace).and_return(test_trace) + allow(File).to receive(:exist?).with(build.path_to_trace).and_return(false) + allow(File).to receive(:exist?).with(build.old_path_to_trace).and_return(false) + end + + it { expect(build.has_trace_file?).to be_falsey } + it { expect(build.trace).to eq(test_trace) } + end + end + end + + describe '#trace_file_path' do + context 'when trace is stored in file' do + before do + allow(build).to receive(:has_trace_file?).and_return(true) + allow(build).to receive(:has_old_trace_file?).and_return(false) + end + + it { expect(build.trace_file_path).to eq(build.path_to_trace) } + end + + context 'when trace is stored in old file' do + before do + allow(build).to receive(:has_trace_file?).and_return(true) + allow(build).to receive(:has_old_trace_file?).and_return(true) + end + + it { expect(build.trace_file_path).to eq(build.old_path_to_trace) } + end + end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 721b20e0cb2..fbf945c757c 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -124,21 +124,38 @@ describe Ci::Pipeline, models: true do describe 'state machine' do let(:current) { Time.now.change(usec: 0) } - let(:build) { create :ci_build, name: 'build1', pipeline: pipeline } + let(:build) { create_build('build1', current, 10) } + let(:build_b) { create_build('build2', current, 20) } + let(:build_c) { create_build('build3', current + 50, 10) } describe '#duration' do before do - travel_to(current - 120) do + pipeline.update(created_at: current) + + travel_to(current + 5) do pipeline.run + pipeline.save + end + + travel_to(current + 30) do + build.success end - travel_to(current) do - pipeline.succeed + travel_to(current + 40) do + build_b.drop end + + travel_to(current + 70) do + build_c.success + end + + pipeline.drop end it 'matches sum of builds duration' do - expect(pipeline.reload.duration).to eq(120) + pipeline.reload + + expect(pipeline.duration).to eq(40) end end @@ -169,6 +186,14 @@ describe Ci::Pipeline, models: true do expect(pipeline.reload.finished_at).to be_nil end end + + def create_build(name, queued_at = current, started_from = 0) + create(:ci_build, + name: name, + pipeline: pipeline, + queued_at: queued_at, + started_at: queued_at + started_from) + end end describe '#branch?' do @@ -195,6 +220,36 @@ describe Ci::Pipeline, models: true do end end + context 'with non-empty project' do + let(:project) { create(:project) } + + let(:pipeline) do + create(:ci_pipeline, + project: project, + ref: project.default_branch, + sha: project.commit.sha) + end + + describe '#latest?' do + context 'with latest sha' do + it 'returns true' do + expect(pipeline).to be_latest + end + end + + context 'with not latest sha' do + before do + pipeline.update( + sha: project.commit("#{project.default_branch}~1").sha) + end + + it 'returns false' do + expect(pipeline).not_to be_latest + end + end + end + end + describe '#manual_actions' do subject { pipeline.manual_actions } diff --git a/spec/models/commit_range_spec.rb b/spec/models/commit_range_spec.rb index 384a38ebc69..c41359b55a3 100644 --- a/spec/models/commit_range_spec.rb +++ b/spec/models/commit_range_spec.rb @@ -76,16 +76,6 @@ describe CommitRange, models: true do end end - describe '#reference_title' do - it 'returns the correct String for three-dot ranges' do - expect(range.reference_title).to eq "Commits #{full_sha_from} through #{full_sha_to}" - end - - it 'returns the correct String for two-dot ranges' do - expect(range2.reference_title).to eq "Commits #{full_sha_from}^ through #{full_sha_to}" - end - end - describe '#to_param' do it 'includes the correct keys' do expect(range.to_param.keys).to eq %i(from to) diff --git a/spec/models/concerns/awardable_spec.rb b/spec/models/concerns/awardable_spec.rb index a371c4a18a9..de791abdf3d 100644 --- a/spec/models/concerns/awardable_spec.rb +++ b/spec/models/concerns/awardable_spec.rb @@ -45,4 +45,14 @@ describe Issue, "Awardable" do expect { issue.toggle_award_emoji("thumbsdown", award_emoji.user) }.to change { AwardEmoji.count }.by(-1) end end + + describe 'querying award_emoji on an Awardable' do + let(:issue) { create(:issue) } + + it 'sorts in ascending fashion' do + create_list(:award_emoji, 3, awardable: issue) + + expect(issue.award_emoji).to eq issue.award_emoji.sort_by(&:id) + end + end end diff --git a/spec/models/concerns/statuseable_spec.rb b/spec/models/concerns/has_status_spec.rb index 8e0a2a2cbde..e118432d098 100644 --- a/spec/models/concerns/statuseable_spec.rb +++ b/spec/models/concerns/has_status_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' -describe Statuseable do +describe HasStatus do before do @object = Object.new - @object.extend(Statuseable::ClassMethods) + @object.extend(HasStatus::ClassMethods) end describe '.status' do @@ -12,7 +12,7 @@ describe Statuseable do end subject { @object.status } - + shared_examples 'build status summary' do context 'all successful' do let(:statuses) { Array.new(2) { create(type, status: :success) } } diff --git a/spec/models/concerns/project_features_compatibility_spec.rb b/spec/models/concerns/project_features_compatibility_spec.rb new file mode 100644 index 00000000000..5363aea4d22 --- /dev/null +++ b/spec/models/concerns/project_features_compatibility_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe ProjectFeaturesCompatibility do + let(:project) { create(:project) } + let(:features) { %w(issues wiki builds merge_requests snippets) } + + # We had issues_enabled, snippets_enabled, builds_enabled, merge_requests_enabled and issues_enabled fields on projects table + # All those fields got moved to a new table called project_feature and are now integers instead of booleans + # This spec tests if the described concern makes sure parameters received by the API are correctly parsed to the new table + # So we can keep it compatible + + it "converts fields from 'true' to ProjectFeature::ENABLED" do + features.each do |feature| + project.update_attribute("#{feature}_enabled".to_sym, "true") + expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::ENABLED) + end + end + + it "converts fields from 'false' to ProjectFeature::DISABLED" do + features.each do |feature| + project.update_attribute("#{feature}_enabled".to_sym, "false") + expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::DISABLED) + end + end +end diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb index 6a640474cfe..3db5937a4f3 100644 --- a/spec/models/diff_note_spec.rb +++ b/spec/models/diff_note_spec.rb @@ -31,6 +31,43 @@ describe DiffNote, models: true do subject { create(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request) } + describe ".resolve!" do + let(:current_user) { create(:user) } + let!(:commit_note) { create(:diff_note_on_commit) } + let!(:resolved_note) { create(:diff_note_on_merge_request, :resolved) } + let!(:unresolved_note) { create(:diff_note_on_merge_request) } + + before do + described_class.resolve!(current_user) + + commit_note.reload + resolved_note.reload + unresolved_note.reload + end + + it 'resolves only the resolvable, not yet resolved notes' do + expect(commit_note.resolved_at).to be_nil + expect(resolved_note.resolved_by).not_to eq(current_user) + expect(unresolved_note.resolved_at).not_to be_nil + expect(unresolved_note.resolved_by).to eq(current_user) + end + end + + describe ".unresolve!" do + let!(:resolved_note) { create(:diff_note_on_merge_request, :resolved) } + + before do + described_class.unresolve! + + resolved_note.reload + end + + it 'unresolves the resolved notes' do + expect(resolved_note.resolved_by).to be_nil + expect(resolved_note.resolved_at).to be_nil + end + end + describe "#position=" do context "when provided a string" do it "sets the position" do diff --git a/spec/models/discussion_spec.rb b/spec/models/discussion_spec.rb index 179f2e73662..0142706d140 100644 --- a/spec/models/discussion_spec.rb +++ b/spec/models/discussion_spec.rb @@ -238,27 +238,19 @@ describe Discussion, model: true do context "when resolvable" do let(:user) { create(:user) } + let(:second_note) { create(:diff_note_on_commit) } # unresolvable before do allow(subject).to receive(:resolvable?).and_return(true) - - allow(first_note).to receive(:resolvable?).and_return(true) - allow(second_note).to receive(:resolvable?).and_return(false) - allow(third_note).to receive(:resolvable?).and_return(true) end context "when all resolvable notes are resolved" do before do first_note.resolve!(user) third_note.resolve!(user) - end - it "calls resolve! on every resolvable note" do - expect(first_note).to receive(:resolve!).with(current_user) - expect(second_note).not_to receive(:resolve!) - expect(third_note).to receive(:resolve!).with(current_user) - - subject.resolve!(current_user) + first_note.reload + third_note.reload end it "doesn't change resolved_at on the resolved notes" do @@ -309,46 +301,44 @@ describe Discussion, model: true do first_note.resolve!(user) end - it "calls resolve! on every resolvable note" do - expect(first_note).to receive(:resolve!).with(current_user) - expect(second_note).not_to receive(:resolve!) - expect(third_note).to receive(:resolve!).with(current_user) - - subject.resolve!(current_user) - end - it "doesn't change resolved_at on the resolved note" do expect(first_note.resolved_at).not_to be_nil - expect { subject.resolve!(current_user) }.not_to change { first_note.resolved_at } + expect { subject.resolve!(current_user) }. + not_to change { first_note.reload.resolved_at } end it "doesn't change resolved_by on the resolved note" do expect(first_note.resolved_by).to eq(user) - expect { subject.resolve!(current_user) }.not_to change { first_note.resolved_by } + expect { subject.resolve!(current_user) }. + not_to change { first_note.reload && first_note.resolved_by } end it "doesn't change the resolved state on the resolved note" do expect(first_note.resolved?).to be true - expect { subject.resolve!(current_user) }.not_to change { first_note.resolved? } + expect { subject.resolve!(current_user) }. + not_to change { first_note.reload && first_note.resolved? } end it "sets resolved_at on the unresolved note" do subject.resolve!(current_user) + third_note.reload expect(third_note.resolved_at).not_to be_nil end it "sets resolved_by on the unresolved note" do subject.resolve!(current_user) + third_note.reload expect(third_note.resolved_by).to eq(current_user) end it "marks the unresolved note as resolved" do subject.resolve!(current_user) + third_note.reload expect(third_note.resolved?).to be true end @@ -373,16 +363,10 @@ describe Discussion, model: true do end context "when no resolvable notes are resolved" do - it "calls resolve! on every resolvable note" do - expect(first_note).to receive(:resolve!).with(current_user) - expect(second_note).not_to receive(:resolve!) - expect(third_note).to receive(:resolve!).with(current_user) - - subject.resolve!(current_user) - end - it "sets resolved_at on the unresolved notes" do subject.resolve!(current_user) + first_note.reload + third_note.reload expect(first_note.resolved_at).not_to be_nil expect(third_note.resolved_at).not_to be_nil @@ -390,6 +374,8 @@ describe Discussion, model: true do it "sets resolved_by on the unresolved notes" do subject.resolve!(current_user) + first_note.reload + third_note.reload expect(first_note.resolved_by).to eq(current_user) expect(third_note.resolved_by).to eq(current_user) @@ -397,6 +383,8 @@ describe Discussion, model: true do it "marks the unresolved notes as resolved" do subject.resolve!(current_user) + first_note.reload + third_note.reload expect(first_note.resolved?).to be true expect(third_note.resolved?).to be true @@ -404,18 +392,24 @@ describe Discussion, model: true do it "sets resolved_at" do subject.resolve!(current_user) + first_note.reload + third_note.reload expect(subject.resolved_at).not_to be_nil end it "sets resolved_by" do subject.resolve!(current_user) + first_note.reload + third_note.reload expect(subject.resolved_by).to eq(current_user) end it "marks as resolved" do subject.resolve!(current_user) + first_note.reload + third_note.reload expect(subject.resolved?).to be true end @@ -451,16 +445,10 @@ describe Discussion, model: true do third_note.resolve!(user) end - it "calls unresolve! on every resolvable note" do - expect(first_note).to receive(:unresolve!) - expect(second_note).not_to receive(:unresolve!) - expect(third_note).to receive(:unresolve!) - - subject.unresolve! - end - it "unsets resolved_at on the resolved notes" do subject.unresolve! + first_note.reload + third_note.reload expect(first_note.resolved_at).to be_nil expect(third_note.resolved_at).to be_nil @@ -468,6 +456,8 @@ describe Discussion, model: true do it "unsets resolved_by on the resolved notes" do subject.unresolve! + first_note.reload + third_note.reload expect(first_note.resolved_by).to be_nil expect(third_note.resolved_by).to be_nil @@ -475,6 +465,8 @@ describe Discussion, model: true do it "unmarks the resolved notes as resolved" do subject.unresolve! + first_note.reload + third_note.reload expect(first_note.resolved?).to be false expect(third_note.resolved?).to be false @@ -482,12 +474,16 @@ describe Discussion, model: true do it "unsets resolved_at" do subject.unresolve! + first_note.reload + third_note.reload expect(subject.resolved_at).to be_nil end it "unsets resolved_by" do subject.unresolve! + first_note.reload + third_note.reload expect(subject.resolved_by).to be_nil end @@ -504,40 +500,22 @@ describe Discussion, model: true do first_note.resolve!(user) end - it "calls unresolve! on every resolvable note" do - expect(first_note).to receive(:unresolve!) - expect(second_note).not_to receive(:unresolve!) - expect(third_note).to receive(:unresolve!) - - subject.unresolve! - end - it "unsets resolved_at on the resolved note" do subject.unresolve! - expect(first_note.resolved_at).to be_nil + expect(subject.first_note.resolved_at).to be_nil end it "unsets resolved_by on the resolved note" do subject.unresolve! - expect(first_note.resolved_by).to be_nil + expect(subject.first_note.resolved_by).to be_nil end it "unmarks the resolved note as resolved" do subject.unresolve! - expect(first_note.resolved?).to be false - end - end - - context "when no resolvable notes are resolved" do - it "calls unresolve! on every resolvable note" do - expect(first_note).to receive(:unresolve!) - expect(second_note).not_to receive(:unresolve!) - expect(third_note).to receive(:unresolve!) - - subject.unresolve! + expect(subject.first_note.resolved?).to be false end end end diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb index 913d74645a7..be57957b569 100644 --- a/spec/models/members/project_member_spec.rb +++ b/spec/models/members/project_member_spec.rb @@ -71,9 +71,6 @@ describe ProjectMember, models: true do describe :import_team do before do - @abilities = Six.new - @abilities << Ability - @project_1 = create :project @project_2 = create :project @@ -92,8 +89,8 @@ describe ProjectMember, models: true do it { expect(@project_2.users).to include(@user_1) } it { expect(@project_2.users).to include(@user_2) } - it { expect(@abilities.allowed?(@user_1, :create_project, @project_2)).to be_truthy } - it { expect(@abilities.allowed?(@user_2, :read_project, @project_2)).to be_truthy } + it { expect(Ability.allowed?(@user_1, :create_project, @project_2)).to be_truthy } + it { expect(Ability.allowed?(@user_2, :read_project, @project_2)).to be_truthy } end describe 'project 1 should not be changed' do diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb index 29f7396f862..e5b185dc3f6 100644 --- a/spec/models/merge_request_diff_spec.rb +++ b/spec/models/merge_request_diff_spec.rb @@ -1,6 +1,27 @@ require 'spec_helper' describe MergeRequestDiff, models: true do + describe 'create new record' do + subject { create(:merge_request).merge_request_diff } + + it { expect(subject).to be_valid } + it { expect(subject).to be_persisted } + it { expect(subject.commits.count).to eq(5) } + it { expect(subject.diffs.count).to eq(8) } + it { expect(subject.head_commit_sha).to eq('5937ac0a7beb003549fc5fd26fc247adbce4a52e') } + it { expect(subject.base_commit_sha).to eq('ae73cb07c9eeaf35924a10f713b364d32b2dd34f') } + it { expect(subject.start_commit_sha).to eq('0b4bc9a49b562e85de7cc9e834518ea6828729b9') } + end + + describe '#latest' do + let!(:mr) { create(:merge_request, :with_diffs) } + let!(:first_diff) { mr.merge_request_diff } + let!(:last_diff) { mr.create_merge_request_diff } + + it { expect(last_diff.latest?).to be_truthy } + it { expect(first_diff.latest?).to be_falsey } + end + describe '#diffs' do let(:mr) { create(:merge_request, :with_diffs) } let(:mr_diff) { mr.merge_request_diff } diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 64c56d922ff..5bf3b8e609e 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -9,7 +9,7 @@ describe MergeRequest, models: true do it { is_expected.to belong_to(:target_project).with_foreign_key(:target_project_id).class_name('Project') } it { is_expected.to belong_to(:source_project).with_foreign_key(:source_project_id).class_name('Project') } it { is_expected.to belong_to(:merge_user).class_name("User") } - it { is_expected.to have_one(:merge_request_diff).dependent(:destroy) } + it { is_expected.to have_many(:merge_request_diffs).dependent(:destroy) } end describe 'modules' do @@ -159,7 +159,7 @@ describe MergeRequest, models: true do context 'when there are MR diffs' do it 'delegates to the MR diffs' do - merge_request.merge_request_diff = MergeRequestDiff.new + merge_request.save expect(merge_request.merge_request_diff).to receive(:raw_diffs).with(hash_including(options)) @@ -316,7 +316,7 @@ describe MergeRequest, models: true do end it "can be removed if the last commit is the head of the source branch" do - allow(subject.source_project).to receive(:commit).and_return(subject.diff_head_commit) + allow(subject).to receive(:source_branch_head).and_return(subject.diff_head_commit) expect(subject.can_remove_source_branch?(user)).to be_truthy end @@ -477,8 +477,8 @@ describe MergeRequest, models: true do allow(subject).to receive(:diff_head_sha).and_return('123abc') - expect(subject.source_project).to receive(:pipeline). - with('123abc', 'master'). + expect(subject.source_project).to receive(:pipeline_for). + with('master', '123abc'). and_return(pipeline) expect(subject.pipeline).to eq(pipeline) @@ -721,12 +721,15 @@ describe MergeRequest, models: true do let(:commit) { subject.project.commit(sample_commit.id) } - it "reloads the diff content" do - expect(subject.merge_request_diff).to receive(:reload_content) - + it "does not change existing merge request diff" do + expect(subject.merge_request_diff).not_to receive(:save_git_content) subject.reload_diff end + it "creates new merge request diff" do + expect { subject.reload_diff }.to change { subject.merge_request_diffs.count }.by(1) + end + it "executs diff cache service" do expect_any_instance_of(MergeRequests::MergeRequestDiffCacheService).to receive(:execute).with(subject) @@ -736,13 +739,15 @@ describe MergeRequest, models: true do it "updates diff note positions" do old_diff_refs = subject.diff_refs - merge_request_diff = subject.merge_request_diff - # Update merge_request_diff so that #diff_refs will return commit.diff_refs - allow(merge_request_diff).to receive(:reload_content) do - merge_request_diff.base_commit_sha = commit.parent_id - merge_request_diff.start_commit_sha = commit.parent_id - merge_request_diff.head_commit_sha = commit.sha + allow(subject).to receive(:create_merge_request_diff) do + subject.merge_request_diffs.create( + base_commit_sha: commit.parent_id, + start_commit_sha: commit.parent_id, + head_commit_sha: commit.sha + ) + + subject.merge_request_diff(true) end expect(Notes::DiffPositionUpdateService).to receive(:new).with( @@ -752,14 +757,31 @@ describe MergeRequest, models: true do new_diff_refs: commit.diff_refs, paths: note.position.paths ).and_call_original - expect_any_instance_of(Notes::DiffPositionUpdateService).to receive(:execute).with(note) + expect_any_instance_of(Notes::DiffPositionUpdateService).to receive(:execute).with(note) expect_any_instance_of(DiffNote).to receive(:save).once subject.reload_diff end end + describe '#branch_merge_base_commit' do + context 'source and target branch exist' do + it { expect(subject.branch_merge_base_commit.sha).to eq('ae73cb07c9eeaf35924a10f713b364d32b2dd34f') } + it { expect(subject.branch_merge_base_commit).to be_a(Commit) } + end + + context 'when the target branch does not exist' do + before do + subject.project.repository.raw_repository.delete_branch(subject.target_branch) + end + + it 'returns nil' do + expect(subject.branch_merge_base_commit).to be_nil + end + end + end + describe "#diff_sha_refs" do context "with diffs" do subject { create(:merge_request, :with_diffs) } @@ -890,6 +912,19 @@ describe MergeRequest, models: true do expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey end + it 'returns a falsey value when the MR is marked as having conflicts, but has none' do + merge_request = create_merge_request('master') + + expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey + end + + it 'returns a falsey value when the MR has a missing ref after a force push' do + merge_request = create_merge_request('conflict-resolvable') + allow(merge_request.conflicts).to receive(:merge_index).and_raise(Rugged::OdbError) + + expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey + end + it 'returns a falsey value when the MR does not support new diff notes' do merge_request = create_merge_request('conflict-resolvable') merge_request.merge_request_diff.update_attributes(start_commit_sha: nil) @@ -927,4 +962,80 @@ describe MergeRequest, models: true do expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_truthy end end + + describe "#forked_source_project_missing?" do + let(:project) { create(:project) } + let(:fork_project) { create(:project, forked_from_project: project) } + let(:user) { create(:user) } + let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } + + context "when the fork exists" do + let(:merge_request) do + create(:merge_request, + source_project: fork_project, + target_project: project) + end + + it { expect(merge_request.forked_source_project_missing?).to be_falsey } + end + + context "when the source project is the same as the target project" do + let(:merge_request) { create(:merge_request, source_project: project) } + + it { expect(merge_request.forked_source_project_missing?).to be_falsey } + end + + context "when the fork does not exist" do + let(:merge_request) do + create(:merge_request, + source_project: fork_project, + target_project: project) + end + + it "returns true" do + unlink_project.execute + merge_request.reload + + expect(merge_request.forked_source_project_missing?).to be_truthy + end + end + end + + describe "#closed_without_fork?" do + let(:project) { create(:project) } + let(:fork_project) { create(:project, forked_from_project: project) } + let(:user) { create(:user) } + let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } + + context "when the merge request is closed" do + let(:closed_merge_request) do + create(:closed_merge_request, + source_project: fork_project, + target_project: project) + end + + it "returns false if the fork exist" do + expect(closed_merge_request.closed_without_fork?).to be_falsey + end + + it "returns true if the fork does not exist" do + unlink_project.execute + closed_merge_request.reload + + expect(closed_merge_request.closed_without_fork?).to be_truthy + end + end + + context "when the merge request is open" do + let(:open_merge_request) do + create(:merge_request, + source_project: fork_project, + target_project: project) + end + + it "returns false" do + expect(open_merge_request.closed_without_fork?).to be_falsey + end + end + end end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index ef2747046b9..e6b6e7c0634 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -85,8 +85,6 @@ describe Note, models: true do @u1 = create(:user) @u2 = create(:user) @u3 = create(:user) - @abilities = Six.new - @abilities << Ability end describe 'read' do @@ -95,9 +93,9 @@ describe Note, models: true do @p2.project_members.create(user: @u3, access_level: ProjectMember::GUEST) end - it { expect(@abilities.allowed?(@u1, :read_note, @p1)).to be_falsey } - it { expect(@abilities.allowed?(@u2, :read_note, @p1)).to be_truthy } - it { expect(@abilities.allowed?(@u3, :read_note, @p1)).to be_falsey } + it { expect(Ability.allowed?(@u1, :read_note, @p1)).to be_falsey } + it { expect(Ability.allowed?(@u2, :read_note, @p1)).to be_truthy } + it { expect(Ability.allowed?(@u3, :read_note, @p1)).to be_falsey } end describe 'write' do @@ -106,9 +104,9 @@ describe Note, models: true do @p2.project_members.create(user: @u3, access_level: ProjectMember::DEVELOPER) end - it { expect(@abilities.allowed?(@u1, :create_note, @p1)).to be_falsey } - it { expect(@abilities.allowed?(@u2, :create_note, @p1)).to be_truthy } - it { expect(@abilities.allowed?(@u3, :create_note, @p1)).to be_falsey } + it { expect(Ability.allowed?(@u1, :create_note, @p1)).to be_falsey } + it { expect(Ability.allowed?(@u2, :create_note, @p1)).to be_truthy } + it { expect(Ability.allowed?(@u3, :create_note, @p1)).to be_falsey } end describe 'admin' do @@ -118,9 +116,9 @@ describe Note, models: true do @p2.project_members.create(user: @u3, access_level: ProjectMember::MASTER) end - it { expect(@abilities.allowed?(@u1, :admin_note, @p1)).to be_falsey } - it { expect(@abilities.allowed?(@u2, :admin_note, @p1)).to be_truthy } - it { expect(@abilities.allowed?(@u3, :admin_note, @p1)).to be_falsey } + it { expect(Ability.allowed?(@u1, :admin_note, @p1)).to be_falsey } + it { expect(Ability.allowed?(@u2, :admin_note, @p1)).to be_truthy } + it { expect(Ability.allowed?(@u3, :admin_note, @p1)).to be_falsey } end end @@ -225,7 +223,7 @@ describe Note, models: true do let(:note) do create :note, noteable: ext_issue, project: ext_proj, - note: "mentioned in issue #{private_issue.to_reference(ext_proj)}", + note: "Mentioned in issue #{private_issue.to_reference(ext_proj)}", system: true end diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb new file mode 100644 index 00000000000..8d554a01be5 --- /dev/null +++ b/spec/models/project_feature_spec.rb @@ -0,0 +1,91 @@ +require 'spec_helper' + +describe ProjectFeature do + let(:project) { create(:project) } + let(:user) { create(:user) } + + describe '#feature_available?' do + let(:features) { %w(issues wiki builds merge_requests snippets) } + + context 'when features are disabled' do + it "returns false" do + features.each do |feature| + project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::DISABLED) + expect(project.feature_available?(:issues, user)).to eq(false) + end + end + end + + context 'when features are enabled only for team members' do + it "returns false when user is not a team member" do + features.each do |feature| + project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE) + expect(project.feature_available?(:issues, user)).to eq(false) + end + end + + it "returns true when user is a team member" do + project.team << [user, :developer] + + features.each do |feature| + project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE) + expect(project.feature_available?(:issues, user)).to eq(true) + end + end + + it "returns true when user is a member of project group" do + group = create(:group) + project = create(:project, namespace: group) + group.add_developer(user) + + features.each do |feature| + project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE) + expect(project.feature_available?(:issues, user)).to eq(true) + end + end + + it "returns true if user is an admin" do + user.update_attribute(:admin, true) + + features.each do |feature| + project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE) + expect(project.feature_available?(:issues, user)).to eq(true) + end + end + end + + context 'when feature is enabled for everyone' do + it "returns true" do + features.each do |feature| + project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::ENABLED) + expect(project.feature_available?(:issues, user)).to eq(true) + end + end + end + end + + describe '#*_enabled?' do + let(:features) { %w(wiki builds merge_requests) } + + it "returns false when feature is disabled" do + features.each do |feature| + project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::DISABLED) + expect(project.public_send("#{feature}_enabled?")).to eq(false) + end + end + + it "returns true when feature is enabled only for team members" do + features.each do |feature| + project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE) + expect(project.public_send("#{feature}_enabled?")).to eq(true) + end + end + + it "returns true when feature is enabled for everyone" do + features.each do |feature| + project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::ENABLED) + expect(project.public_send("#{feature}_enabled?")).to eq(true) + end + end + end +end diff --git a/spec/models/project_security_spec.rb b/spec/models/project_security_spec.rb deleted file mode 100644 index 36379074ea0..00000000000 --- a/spec/models/project_security_spec.rb +++ /dev/null @@ -1,112 +0,0 @@ -require 'spec_helper' - -describe Project, models: true do - describe 'authorization' do - before do - @p1 = create(:project) - - @u1 = create(:user) - @u2 = create(:user) - @u3 = create(:user) - @u4 = @p1.owner - - @abilities = Six.new - @abilities << Ability - end - - let(:guest_actions) { Ability.project_guest_rules } - let(:report_actions) { Ability.project_report_rules } - let(:dev_actions) { Ability.project_dev_rules } - let(:master_actions) { Ability.project_master_rules } - let(:owner_actions) { Ability.project_owner_rules } - - describe "Non member rules" do - it "denies for non-project users any actions" do - owner_actions.each do |action| - expect(@abilities.allowed?(@u1, action, @p1)).to be_falsey - end - end - end - - describe "Guest Rules" do - before do - @p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::GUEST) - end - - it "allows for project user any guest actions" do - guest_actions.each do |action| - expect(@abilities.allowed?(@u2, action, @p1)).to be_truthy - end - end - end - - describe "Report Rules" do - before do - @p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::REPORTER) - end - - it "allows for project user any report actions" do - report_actions.each do |action| - expect(@abilities.allowed?(@u2, action, @p1)).to be_truthy - end - end - end - - describe "Developer Rules" do - before do - @p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::REPORTER) - @p1.project_members.create(project: @p1, user: @u3, access_level: ProjectMember::DEVELOPER) - end - - it "denies for developer master-specific actions" do - [dev_actions - report_actions].each do |action| - expect(@abilities.allowed?(@u2, action, @p1)).to be_falsey - end - end - - it "allows for project user any dev actions" do - dev_actions.each do |action| - expect(@abilities.allowed?(@u3, action, @p1)).to be_truthy - end - end - end - - describe "Master Rules" do - before do - @p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::DEVELOPER) - @p1.project_members.create(project: @p1, user: @u3, access_level: ProjectMember::MASTER) - end - - it "denies for developer master-specific actions" do - [master_actions - dev_actions].each do |action| - expect(@abilities.allowed?(@u2, action, @p1)).to be_falsey - end - end - - it "allows for project user any master actions" do - master_actions.each do |action| - expect(@abilities.allowed?(@u3, action, @p1)).to be_truthy - end - end - end - - describe "Owner Rules" do - before do - @p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::DEVELOPER) - @p1.project_members.create(project: @p1, user: @u3, access_level: ProjectMember::MASTER) - end - - it "denies for masters admin-specific actions" do - [owner_actions - master_actions].each do |action| - expect(@abilities.allowed?(@u2, action, @p1)).to be_falsey - end - end - - it "allows for project owner any admin actions" do - owner_actions.each do |action| - expect(@abilities.allowed?(@u4, action, @p1)).to be_truthy - end - end - end - end -end diff --git a/spec/models/project_services/slack_service/build_message_spec.rb b/spec/models/project_services/slack_service/build_message_spec.rb index 7fcfdf0eacd..452f4e2782c 100644 --- a/spec/models/project_services/slack_service/build_message_spec.rb +++ b/spec/models/project_services/slack_service/build_message_spec.rb @@ -10,7 +10,7 @@ describe SlackService::BuildMessage do tag: false, project_name: 'project_name', - project_url: 'somewhere.com', + project_url: 'example.gitlab.com', commit: { status: status, @@ -20,42 +20,38 @@ describe SlackService::BuildMessage do } end - context 'succeeded' do + let(:message) { build_message } + + context 'build succeeded' do let(:status) { 'success' } let(:color) { 'good' } let(:duration) { 10 } - + let(:message) { build_message('passed') } + it 'returns a message with information about succeeded build' do - message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker passed in 10 seconds' expect(subject.pretext).to be_empty expect(subject.fallback).to eq(message) expect(subject.attachments).to eq([text: message, color: color]) end end - context 'failed' do + context 'build failed' do let(:status) { 'failed' } let(:color) { 'danger' } let(:duration) { 10 } it 'returns a message with information about failed build' do - message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker failed in 10 seconds' expect(subject.pretext).to be_empty expect(subject.fallback).to eq(message) expect(subject.attachments).to eq([text: message, color: color]) end - end - - describe '#seconds_name' do - let(:status) { 'failed' } - let(:color) { 'danger' } - let(:duration) { 1 } + end - it 'returns seconds as singular when there is only one' do - message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker failed in 1 second' - expect(subject.pretext).to be_empty - expect(subject.fallback).to eq(message) - expect(subject.attachments).to eq([text: message, color: color]) - end + def build_message(status_text = status) + "<example.gitlab.com|project_name>:" \ + " Commit <example.gitlab.com/commit/" \ + "97de212e80737a608d939f648d959671fb0a0142/builds|97de212e>" \ + " of <example.gitlab.com/commits/develop|develop> branch" \ + " by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}" end end diff --git a/spec/models/project_services/slack_service/pipeline_message_spec.rb b/spec/models/project_services/slack_service/pipeline_message_spec.rb new file mode 100644 index 00000000000..babb3909f56 --- /dev/null +++ b/spec/models/project_services/slack_service/pipeline_message_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe SlackService::PipelineMessage do + subject { SlackService::PipelineMessage.new(args) } + + let(:args) do + { + object_attributes: { + id: 123, + sha: '97de212e80737a608d939f648d959671fb0a0142', + tag: false, + ref: 'develop', + status: status, + duration: duration + }, + project: { path_with_namespace: 'project_name', + web_url: 'example.gitlab.com' }, + commit: { author_name: 'hacker' } + } + end + + let(:message) { build_message } + + context 'pipeline succeeded' do + let(:status) { 'success' } + let(:color) { 'good' } + let(:duration) { 10 } + let(:message) { build_message('passed') } + + it 'returns a message with information about succeeded build' do + expect(subject.pretext).to be_empty + expect(subject.fallback).to eq(message) + expect(subject.attachments).to eq([text: message, color: color]) + end + end + + context 'pipeline failed' do + let(:status) { 'failed' } + let(:color) { 'danger' } + let(:duration) { 10 } + + it 'returns a message with information about failed build' do + expect(subject.pretext).to be_empty + expect(subject.fallback).to eq(message) + expect(subject.attachments).to eq([text: message, color: color]) + end + end + + def build_message(status_text = status) + "<example.gitlab.com|project_name>:" \ + " Pipeline <example.gitlab.com/pipelines/123|97de212e>" \ + " of <example.gitlab.com/commits/develop|develop> branch" \ + " by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}" + end +end diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb index 28af68d13b4..5afdc4b2f7b 100644 --- a/spec/models/project_services/slack_service_spec.rb +++ b/spec/models/project_services/slack_service_spec.rb @@ -21,6 +21,9 @@ require 'spec_helper' describe SlackService, models: true do + let(:slack) { SlackService.new } + let(:webhook_url) { 'https://example.gitlab.com/' } + describe "Associations" do it { is_expected.to belong_to :project } it { is_expected.to have_one :service_hook } @@ -42,15 +45,14 @@ describe SlackService, models: true do end describe "Execute" do - let(:slack) { SlackService.new } let(:user) { create(:user) } let(:project) { create(:project) } + let(:username) { 'slack_username' } + let(:channel) { 'slack_channel' } + let(:push_sample_data) do Gitlab::DataBuilder::Push.build_sample(project, user) end - let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' } - let(:username) { 'slack_username' } - let(:channel) { 'slack_channel' } before do allow(slack).to receive_messages( @@ -212,10 +214,8 @@ describe SlackService, models: true do end describe "Note events" do - let(:slack) { SlackService.new } let(:user) { create(:user) } let(:project) { create(:project, creator_id: user.id) } - let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' } before do allow(slack).to receive_messages( @@ -285,4 +285,63 @@ describe SlackService, models: true do end end end + + describe 'Pipeline events' do + let(:user) { create(:user) } + let(:project) { create(:project) } + + let(:pipeline) do + create(:ci_pipeline, + project: project, status: status, + sha: project.commit.sha, ref: project.default_branch) + end + + before do + allow(slack).to receive_messages( + project: project, + service_hook: true, + webhook: webhook_url + ) + end + + shared_examples 'call Slack API' do + before do + WebMock.stub_request(:post, webhook_url) + end + + it 'calls Slack API for pipeline events' do + data = Gitlab::DataBuilder::Pipeline.build(pipeline) + slack.execute(data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + end + + context 'with failed pipeline' do + let(:status) { 'failed' } + + it_behaves_like 'call Slack API' + end + + context 'with succeeded pipeline' do + let(:status) { 'success' } + + context 'with default to notify_only_broken_pipelines' do + it 'does not call Slack API for pipeline events' do + data = Gitlab::DataBuilder::Pipeline.build(pipeline) + result = slack.execute(data) + + expect(result).to be_falsy + end + end + + context 'with setting notify_only_broken_pipelines to false' do + before do + slack.notify_only_broken_pipelines = false + end + + it_behaves_like 'call Slack API' + end + end + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 9a3660012f9..4a41fafb84d 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -506,6 +506,18 @@ describe Project, models: true do end end + describe '#has_wiki?' do + let(:no_wiki_project) { build(:project, wiki_enabled: false, has_external_wiki: false) } + let(:wiki_enabled_project) { build(:project) } + let(:external_wiki_project) { build(:project, has_external_wiki: true) } + + it 'returns true if project is wiki enabled or has external wiki' do + expect(wiki_enabled_project).to have_wiki + expect(external_wiki_project).to have_wiki + expect(no_wiki_project).not_to have_wiki + end + end + describe '#external_wiki' do let(:project) { create(:project) } @@ -685,31 +697,43 @@ describe Project, models: true do end end - describe '#pipeline' do - let(:project) { create :project } - let(:pipeline) { create :ci_pipeline, project: project, ref: 'master' } - - subject { project.pipeline(pipeline.sha, 'master') } + describe '#pipeline_for' do + let(:project) { create(:project) } + let!(:pipeline) { create_pipeline } - it { is_expected.to eq(pipeline) } + shared_examples 'giving the correct pipeline' do + it { is_expected.to eq(pipeline) } - context 'return latest' do - let(:pipeline2) { create :ci_pipeline, project: project, ref: 'master' } + context 'return latest' do + let!(:pipeline2) { create_pipeline } - before do - pipeline - pipeline2 + it { is_expected.to eq(pipeline2) } end + end + + context 'with explicit sha' do + subject { project.pipeline_for('master', pipeline.sha) } + + it_behaves_like 'giving the correct pipeline' + end + + context 'with implicit sha' do + subject { project.pipeline_for('master') } + + it_behaves_like 'giving the correct pipeline' + end - it { is_expected.to eq(pipeline2) } + def create_pipeline + create(:ci_pipeline, + project: project, + ref: 'master', + sha: project.commit('master').sha) end end describe '#builds_enabled' do let(:project) { create :project } - before { project.builds_enabled = true } - subject { project.builds_enabled } it { expect(project.builds_enabled?).to be_truthy } @@ -1442,4 +1466,35 @@ describe Project, models: true do expect(shared_project.authorized_for_user?(master, Gitlab::Access::MASTER)).to be(true) end end + + describe 'change_head' do + let(:project) { create(:project) } + + it 'calls the before_change_head method' do + expect(project.repository).to receive(:before_change_head) + project.change_head(project.default_branch) + end + + it 'creates the new reference with rugged' do + expect(project.repository.rugged.references).to receive(:create).with('HEAD', + "refs/heads/#{project.default_branch}", + force: true) + project.change_head(project.default_branch) + end + + it 'copies the gitattributes' do + expect(project.repository).to receive(:copy_gitattributes).with(project.default_branch) + project.change_head(project.default_branch) + end + + it 'expires the avatar cache' do + expect(project.repository).to receive(:expire_avatar_cache).with(project.default_branch) + project.change_head(project.default_branch) + end + + it 'reloads the default branch' do + expect(project).to receive(:reload_default_branch) + project.change_head(project.default_branch) + end + end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 1fea50ad42c..7624050878e 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -382,6 +382,24 @@ describe Repository, models: true do end end + describe '#find_branch' do + it 'loads a branch with a fresh repo' do + expect(Gitlab::Git::Repository).to receive(:new).twice.and_call_original + + 2.times do + expect(repository.find_branch('feature')).not_to be_nil + end + end + + it 'loads a branch with a cached repo' do + expect(Gitlab::Git::Repository).to receive(:new).once.and_call_original + + 2.times do + expect(repository.find_branch('feature', fresh_repo: false)).not_to be_nil + end + end + end + describe '#rm_branch' do let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature let(:blank_sha) { '0000000000000000000000000000000000000000' } @@ -423,43 +441,77 @@ describe Repository, models: true do end end - describe '#commit_with_hooks' do + describe '#update_branch_with_hooks' do let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature + let(:new_rev) { 'a74ae73c1ccde9b974a70e82b901588071dc142a' } # commit whose parent is old_rev context 'when pre hooks were successful' do before do expect_any_instance_of(GitHooksService).to receive(:execute). - with(user, repository.path_to_repo, old_rev, sample_commit.id, 'refs/heads/feature'). + with(user, repository.path_to_repo, old_rev, new_rev, 'refs/heads/feature'). and_yield.and_return(true) end it 'runs without errors' do expect do - repository.commit_with_hooks(user, 'feature') { sample_commit.id } + repository.update_branch_with_hooks(user, 'feature') { new_rev } end.not_to raise_error end it 'ensures the autocrlf Git option is set to :input' do expect(repository).to receive(:update_autocrlf_option) - repository.commit_with_hooks(user, 'feature') { sample_commit.id } + repository.update_branch_with_hooks(user, 'feature') { new_rev } end context "when the branch wasn't empty" do it 'updates the head' do expect(repository.find_branch('feature').target.id).to eq(old_rev) - repository.commit_with_hooks(user, 'feature') { sample_commit.id } - expect(repository.find_branch('feature').target.id).to eq(sample_commit.id) + repository.update_branch_with_hooks(user, 'feature') { new_rev } + expect(repository.find_branch('feature').target.id).to eq(new_rev) end end end + context 'when the update adds more than one commit' do + it 'runs without errors' do + old_rev = '33f3729a45c02fc67d00adb1b8bca394b0e761d9' + + # old_rev is an ancestor of new_rev + expect(repository.rugged.merge_base(old_rev, new_rev)).to eq(old_rev) + + # old_rev is not a direct ancestor (parent) of new_rev + expect(repository.rugged.lookup(new_rev).parent_ids).not_to include(old_rev) + + branch = 'feature-ff-target' + repository.add_branch(user, branch, old_rev) + + expect { repository.update_branch_with_hooks(user, branch) { new_rev } }.not_to raise_error + end + end + + context 'when the update would remove commits from the target branch' do + it 'raises an exception' do + branch = 'master' + old_rev = repository.find_branch(branch).target.sha + + # The 'master' branch is NOT an ancestor of new_rev. + expect(repository.rugged.merge_base(old_rev, new_rev)).not_to eq(old_rev) + + # Updating 'master' to new_rev would lose the commits on 'master' that + # are not contained in new_rev. This should not be allowed. + expect do + repository.update_branch_with_hooks(user, branch) { new_rev } + end.to raise_error(Repository::CommitError) + end + end + context 'when pre hooks failed' do it 'gets an error' do allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, '']) expect do - repository.commit_with_hooks(user, 'feature') { sample_commit.id } + repository.update_branch_with_hooks(user, 'feature') { new_rev } end.to raise_error(GitHooksService::PreReceiveError) end end @@ -467,6 +519,7 @@ describe Repository, models: true do context 'when target branch is different from source branch' do before do allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, '']) + allow(repository).to receive(:update_ref!) end it 'expires branch cache' do @@ -477,7 +530,7 @@ describe Repository, models: true do expect(repository).to receive(:expire_has_visible_content_cache) expect(repository).to receive(:expire_branch_count_cache) - repository.commit_with_hooks(user, 'new-feature') { sample_commit.id } + repository.update_branch_with_hooks(user, 'new-feature') { new_rev } end end @@ -1250,4 +1303,18 @@ describe Repository, models: true do File.delete(path) end end + + describe '#update_ref!' do + it 'can create a ref' do + repository.update_ref!('refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA) + + expect(repository.find_branch('foobar')).not_to be_nil + end + + it 'raises CommitError when the ref update fails' do + expect do + repository.update_ref!('refs/heads/master', 'refs/heads/master', Gitlab::Git::BLANK_SHA) + end.to raise_error(Repository::CommitError) + end + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 8eb0c5033c9..a1770d96f83 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1006,8 +1006,7 @@ describe User, models: true do end it 'does not include projects for which issues are disabled' do - project = create(:project) - project.update_attributes(issues_enabled: false) + project = create(:project, issues_access_level: ProjectFeature::DISABLED) expect(user.projects_where_can_admin_issues.to_a).to be_empty expect(user.can?(:admin_issue, project)).to eq(false) diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb new file mode 100644 index 00000000000..eda1cafd65e --- /dev/null +++ b/spec/policies/project_policy_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe ProjectPolicy, models: true do + let(:project) { create(:empty_project, :public) } + let(:guest) { create(:user) } + let(:reporter) { create(:user) } + let(:dev) { create(:user) } + let(:master) { create(:user) } + let(:owner) { create(:user) } + let(:admin) { create(:admin) } + + let(:users_ordered_by_permissions) do + [nil, guest, reporter, dev, master, owner, admin] + end + + let(:users_permissions) do + users_ordered_by_permissions.map { |u| Ability.allowed(u, project).size } + end + + before do + project.team << [guest, :guest] + project.team << [master, :master] + project.team << [dev, :developer] + project.team << [reporter, :reporter] + + group = create(:group) + project.project_group_links.create( + group: group, + group_access: Gitlab::Access::MASTER) + group.add_owner(owner) + end + + it 'returns increasing permissions for each level' do + expect(users_permissions).to eq(users_permissions.sort.uniq) + end +end diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb index 73c268c0d1e..981a6791881 100644 --- a/spec/requests/api/award_emoji_spec.rb +++ b/spec/requests/api/award_emoji_spec.rb @@ -4,7 +4,7 @@ describe API::API, api: true do include ApiHelpers let(:user) { create(:user) } let!(:project) { create(:project) } - let(:issue) { create(:issue, project: project, author: user) } + let(:issue) { create(:issue, project: project) } let!(:award_emoji) { create(:award_emoji, awardable: issue, user: user) } let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) } let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request, user: user) } @@ -115,6 +115,8 @@ describe API::API, api: true do end describe "POST /projects/:id/awardable/:awardable_id/award_emoji" do + let(:issue2) { create(:issue, project: project, author: user) } + context "on an issue" do it "creates a new award emoji" do post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'blowfish' @@ -136,6 +138,12 @@ describe API::API, api: true do expect(response).to have_http_status(401) end + it "returns a 404 error if the user authored issue" do + post api("/projects/#{project.id}/issues/#{issue2.id}/award_emoji", user), name: 'thumbsup' + + expect(response).to have_http_status(404) + end + it "normalizes +1 as thumbsup award" do post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: '+1' @@ -155,6 +163,8 @@ describe API::API, api: true do end describe "POST /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji" do + let(:note2) { create(:note, project: project, noteable: issue, author: user) } + it 'creates a new award emoji' do expect do post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket' @@ -164,6 +174,12 @@ describe API::API, api: true do expect(json_response['user']['username']).to eq(user.username) end + it "it returns 404 error when user authored note" do + post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note2.id}/award_emoji", user), name: 'thumbsup' + + expect(response).to have_http_status(404) + end + it "normalizes +1 as thumbsup award" do post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: '+1' diff --git a/spec/requests/api/broadcast_messages_spec.rb b/spec/requests/api/broadcast_messages_spec.rb new file mode 100644 index 00000000000..7c9078b2864 --- /dev/null +++ b/spec/requests/api/broadcast_messages_spec.rb @@ -0,0 +1,180 @@ +require 'spec_helper' + +describe API::BroadcastMessages, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:admin) { create(:admin) } + + describe 'GET /broadcast_messages' do + it 'returns a 401 for anonymous users' do + get api('/broadcast_messages') + + expect(response).to have_http_status(401) + end + + it 'returns a 403 for users' do + get api('/broadcast_messages', user) + + expect(response).to have_http_status(403) + end + + it 'returns an Array of BroadcastMessages for admins' do + create(:broadcast_message) + + get api('/broadcast_messages', admin) + + expect(response).to have_http_status(200) + expect(json_response).to be_kind_of(Array) + expect(json_response.first.keys) + .to match_array(%w(id message starts_at ends_at color font active)) + end + end + + describe 'GET /broadcast_messages/:id' do + let!(:message) { create(:broadcast_message) } + + it 'returns a 401 for anonymous users' do + get api("/broadcast_messages/#{message.id}") + + expect(response).to have_http_status(401) + end + + it 'returns a 403 for users' do + get api("/broadcast_messages/#{message.id}", user) + + expect(response).to have_http_status(403) + end + + it 'returns the specified message for admins' do + get api("/broadcast_messages/#{message.id}", admin) + + expect(response).to have_http_status(200) + expect(json_response['id']).to eq message.id + expect(json_response.keys) + .to match_array(%w(id message starts_at ends_at color font active)) + end + end + + describe 'POST /broadcast_messages' do + it 'returns a 401 for anonymous users' do + post api('/broadcast_messages'), attributes_for(:broadcast_message) + + expect(response).to have_http_status(401) + end + + it 'returns a 403 for users' do + post api('/broadcast_messages', user), attributes_for(:broadcast_message) + + expect(response).to have_http_status(403) + end + + context 'as an admin' do + it 'requires the `message` parameter' do + attrs = attributes_for(:broadcast_message) + attrs.delete(:message) + + post api('/broadcast_messages', admin), attrs + + expect(response).to have_http_status(400) + expect(json_response['error']).to eq 'message is missing' + end + + it 'defines sane default start and end times' do + time = Time.zone.parse('2016-07-02 10:11:12') + travel_to(time) do + post api('/broadcast_messages', admin), message: 'Test message' + + expect(response).to have_http_status(201) + expect(json_response['starts_at']).to eq '2016-07-02T10:11:12.000Z' + expect(json_response['ends_at']).to eq '2016-07-02T11:11:12.000Z' + end + end + + it 'accepts a custom background and foreground color' do + attrs = attributes_for(:broadcast_message, color: '#000000', font: '#cecece') + + post api('/broadcast_messages', admin), attrs + + expect(response).to have_http_status(201) + expect(json_response['color']).to eq attrs[:color] + expect(json_response['font']).to eq attrs[:font] + end + end + end + + describe 'PUT /broadcast_messages/:id' do + let!(:message) { create(:broadcast_message) } + + it 'returns a 401 for anonymous users' do + put api("/broadcast_messages/#{message.id}"), + attributes_for(:broadcast_message) + + expect(response).to have_http_status(401) + end + + it 'returns a 403 for users' do + put api("/broadcast_messages/#{message.id}", user), + attributes_for(:broadcast_message) + + expect(response).to have_http_status(403) + end + + context 'as an admin' do + it 'accepts new background and foreground colors' do + attrs = { color: '#000000', font: '#cecece' } + + put api("/broadcast_messages/#{message.id}", admin), attrs + + expect(response).to have_http_status(200) + expect(json_response['color']).to eq attrs[:color] + expect(json_response['font']).to eq attrs[:font] + end + + it 'accepts new start and end times' do + time = Time.zone.parse('2016-07-02 10:11:12') + travel_to(time) do + attrs = { starts_at: Time.zone.now, ends_at: 3.hours.from_now } + + put api("/broadcast_messages/#{message.id}", admin), attrs + + expect(response).to have_http_status(200) + expect(json_response['starts_at']).to eq '2016-07-02T10:11:12.000Z' + expect(json_response['ends_at']).to eq '2016-07-02T13:11:12.000Z' + end + end + + it 'accepts a new message' do + attrs = { message: 'new message' } + + put api("/broadcast_messages/#{message.id}", admin), attrs + + expect(response).to have_http_status(200) + expect { message.reload }.to change { message.message }.to('new message') + end + end + end + + describe 'DELETE /broadcast_messages/:id' do + let!(:message) { create(:broadcast_message) } + + it 'returns a 401 for anonymous users' do + delete api("/broadcast_messages/#{message.id}"), + attributes_for(:broadcast_message) + + expect(response).to have_http_status(401) + end + + it 'returns a 403 for users' do + delete api("/broadcast_messages/#{message.id}", user), + attributes_for(:broadcast_message) + + expect(response).to have_http_status(403) + end + + it 'deletes the broadcast message for admins' do + expect { delete api("/broadcast_messages/#{message.id}", admin) } + .to change { BroadcastMessage.count }.by(-1) + end + end +end diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index 9a17a705b1e..ee0b61e2ca4 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -15,7 +15,9 @@ describe API::API, api: true do describe 'GET /projects/:id/builds ' do let(:query) { '' } - before { get api("/projects/#{project.id}/builds?#{query}", api_user) } + before do + get api("/projects/#{project.id}/builds?#{query}", api_user) + end context 'authorized user' do it 'returns project builds' do @@ -122,7 +124,9 @@ describe API::API, api: true do end describe 'GET /projects/:id/builds/:build_id' do - before { get api("/projects/#{project.id}/builds/#{build.id}", api_user) } + before do + get api("/projects/#{project.id}/builds/#{build.id}", api_user) + end context 'authorized user' do it 'returns specific build data' do @@ -141,7 +145,9 @@ describe API::API, api: true do end describe 'GET /projects/:id/builds/:build_id/artifacts' do - before { get api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user) } + before do + get api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user) + end context 'build with artifacts' do let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } @@ -292,7 +298,9 @@ describe API::API, api: true do end describe 'POST /projects/:id/builds/:build_id/cancel' do - before { post api("/projects/#{project.id}/builds/#{build.id}/cancel", api_user) } + before do + post api("/projects/#{project.id}/builds/#{build.id}/cancel", api_user) + end context 'authorized user' do context 'user with :update_build persmission' do @@ -323,7 +331,9 @@ describe API::API, api: true do describe 'POST /projects/:id/builds/:build_id/retry' do let(:build) { create(:ci_build, :canceled, pipeline: pipeline) } - before { post api("/projects/#{project.id}/builds/#{build.id}/retry", api_user) } + before do + post api("/projects/#{project.id}/builds/#{build.id}/retry", api_user) + end context 'authorized user' do context 'user with :update_build permission' do diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 7ca75d77673..5b3dc60aba2 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -95,7 +95,7 @@ describe API::API, api: true do end it "returns status for CI" do - pipeline = project.ensure_pipeline(project.repository.commit.sha, 'master') + pipeline = project.ensure_pipeline('master', project.repository.commit.sha) pipeline.update(status: 'success') get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) @@ -105,7 +105,7 @@ describe API::API, api: true do end it "returns status for CI when pipeline is created" do - project.ensure_pipeline(project.repository.commit.sha, 'master') + project.ensure_pipeline('master', project.repository.commit.sha) get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) diff --git a/spec/requests/api/fork_spec.rb b/spec/requests/api/fork_spec.rb index f802fcd2d2e..06e3a2183c0 100644 --- a/spec/requests/api/fork_spec.rb +++ b/spec/requests/api/fork_spec.rb @@ -6,6 +6,12 @@ describe API::API, api: true do let(:user2) { create(:user) } let(:user3) { create(:user) } let(:admin) { create(:admin) } + let(:group) { create(:group) } + let(:group2) do + group = create(:group, name: 'group2_name') + group.add_owner(user2) + group + end let(:project) do create(:project, creator_id: user.id, namespace: user.namespace) @@ -22,6 +28,7 @@ describe API::API, api: true do context 'when authenticated' do it 'forks if user has sufficient access to project' do post api("/projects/fork/#{project.id}", user2) + expect(response).to have_http_status(201) expect(json_response['name']).to eq(project.name) expect(json_response['path']).to eq(project.path) @@ -32,6 +39,7 @@ describe API::API, api: true do it 'forks if user is admin' do post api("/projects/fork/#{project.id}", admin) + expect(response).to have_http_status(201) expect(json_response['name']).to eq(project.name) expect(json_response['path']).to eq(project.path) @@ -42,12 +50,14 @@ describe API::API, api: true do it 'fails on missing project access for the project to fork' do post api("/projects/fork/#{project.id}", user3) + expect(response).to have_http_status(404) expect(json_response['message']).to eq('404 Project Not Found') end it 'fails if forked project exists in the user namespace' do post api("/projects/fork/#{project.id}", user) + expect(response).to have_http_status(409) expect(json_response['message']['name']).to eq(['has already been taken']) expect(json_response['message']['path']).to eq(['has already been taken']) @@ -55,14 +65,70 @@ describe API::API, api: true do it 'fails if project to fork from does not exist' do post api('/projects/fork/424242', user) + expect(response).to have_http_status(404) expect(json_response['message']).to eq('404 Project Not Found') end + + it 'forks with explicit own user namespace id' do + post api("/projects/fork/#{project.id}", user2), namespace: user2.namespace.id + + expect(response).to have_http_status(201) + expect(json_response['owner']['id']).to eq(user2.id) + end + + it 'forks with explicit own user name as namespace' do + post api("/projects/fork/#{project.id}", user2), namespace: user2.username + + expect(response).to have_http_status(201) + expect(json_response['owner']['id']).to eq(user2.id) + end + + it 'forks to another user when admin' do + post api("/projects/fork/#{project.id}", admin), namespace: user2.username + + expect(response).to have_http_status(201) + expect(json_response['owner']['id']).to eq(user2.id) + end + + it 'fails if trying to fork to another user when not admin' do + post api("/projects/fork/#{project.id}", user2), namespace: admin.namespace.id + + expect(response).to have_http_status(409) + end + + it 'fails if trying to fork to non-existent namespace' do + post api("/projects/fork/#{project.id}", user2), namespace: 42424242 + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Target Namespace Not Found') + end + + it 'forks to owned group' do + post api("/projects/fork/#{project.id}", user2), namespace: group2.name + + expect(response).to have_http_status(201) + expect(json_response['namespace']['name']).to eq(group2.name) + end + + it 'fails to fork to not owned group' do + post api("/projects/fork/#{project.id}", user2), namespace: group.name + + expect(response).to have_http_status(409) + end + + it 'forks to not owned group when admin' do + post api("/projects/fork/#{project.id}", admin), namespace: group.name + + expect(response).to have_http_status(201) + expect(json_response['namespace']['name']).to eq(group.name) + end end context 'when unauthenticated' do it 'returns authentication error' do post api("/projects/fork/#{project.id}") + expect(response).to have_http_status(401) expect(json_response['message']).to eq('401 Unauthorized') end diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 5d06abcfeb3..46d1b868782 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -44,8 +44,8 @@ describe API::API, api: true do secret_token: secret_token, key_id: 12345 - expect(response).to have_http_status(404) - expect(json_response['message']).to eq('404 Not found') + expect(json_response['success']).to be_falsey + expect(json_response['message']).to eq('Could not find the given key') end it 'returns an error message when the key is a deploy key' do diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index b8038fc85a1..86d994be079 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe API::API, api: true do include ApiHelpers + let(:user) { create(:user) } let(:user2) { create(:user) } let(:non_member) { create(:user) } @@ -16,21 +17,24 @@ describe API::API, api: true do assignee: user, project: project, state: :closed, - milestone: milestone + milestone: milestone, + updated_at: 3.hours.ago end let!(:confidential_issue) do create :issue, :confidential, project: project, author: author, - assignee: assignee + assignee: assignee, + updated_at: 2.hours.ago end let!(:issue) do create :issue, author: user, assignee: user, project: project, - milestone: milestone + milestone: milestone, + updated_at: 1.hour.ago end let!(:label) do create(:label, title: 'label', color: '#FFAABB', project: project) @@ -134,6 +138,42 @@ describe API::API, api: true do expect(json_response).to be_an Array expect(json_response.length).to eq(0) end + + it 'sorts by created_at descending by default' do + get api('/issues', user) + response_dates = json_response.map { |issue| issue['created_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort.reverse) + end + + it 'sorts ascending when requested' do + get api('/issues?sort=asc', user) + response_dates = json_response.map { |issue| issue['created_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort) + end + + it 'sorts by updated_at descending when requested' do + get api('/issues?order_by=updated_at', user) + response_dates = json_response.map { |issue| issue['updated_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort.reverse) + end + + it 'sorts by updated_at ascending when requested' do + get api('/issues?order_by=updated_at&sort=asc', user) + response_dates = json_response.map { |issue| issue['updated_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort) + end end end @@ -146,21 +186,24 @@ describe API::API, api: true do assignee: user, project: group_project, state: :closed, - milestone: group_milestone + milestone: group_milestone, + updated_at: 3.hours.ago end let!(:group_confidential_issue) do create :issue, :confidential, project: group_project, author: author, - assignee: assignee + assignee: assignee, + updated_at: 2.hours.ago end let!(:group_issue) do create :issue, author: user, assignee: user, project: group_project, - milestone: group_milestone + milestone: group_milestone, + updated_at: 1.hour.ago end let!(:group_label) do create(:label, title: 'group_lbl', color: '#FFAABB', project: group_project) @@ -277,6 +320,42 @@ describe API::API, api: true do expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(group_closed_issue.id) end + + it 'sorts by created_at descending by default' do + get api(base_url, user) + response_dates = json_response.map { |issue| issue['created_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort.reverse) + end + + it 'sorts ascending when requested' do + get api("#{base_url}?sort=asc", user) + response_dates = json_response.map { |issue| issue['created_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort) + end + + it 'sorts by updated_at descending when requested' do + get api("#{base_url}?order_by=updated_at", user) + response_dates = json_response.map { |issue| issue['updated_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort.reverse) + end + + it 'sorts by updated_at ascending when requested' do + get api("#{base_url}?order_by=updated_at&sort=asc", user) + response_dates = json_response.map { |issue| issue['updated_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort) + end end describe "GET /projects/:id/issues" do @@ -385,6 +464,42 @@ describe API::API, api: true do expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(closed_issue.id) end + + it 'sorts by created_at descending by default' do + get api("#{base_url}/issues", user) + response_dates = json_response.map { |issue| issue['created_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort.reverse) + end + + it 'sorts ascending when requested' do + get api("#{base_url}/issues?sort=asc", user) + response_dates = json_response.map { |issue| issue['created_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort) + end + + it 'sorts by updated_at descending when requested' do + get api("#{base_url}/issues?order_by=updated_at", user) + response_dates = json_response.map { |issue| issue['updated_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort.reverse) + end + + it 'sorts by updated_at ascending when requested' do + get api("#{base_url}/issues?order_by=updated_at&sort=asc", user) + response_dates = json_response.map { |issue| issue['updated_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort) + end end describe "GET /projects/:id/issues/:issue_id" do @@ -404,6 +519,7 @@ describe API::API, api: true do expect(json_response['milestone']).to be_a Hash expect(json_response['assignee']).to be_a Hash expect(json_response['author']).to be_a Hash + expect(json_response['confidential']).to be_falsy end it "returns a project issue by id" do @@ -469,13 +585,63 @@ describe API::API, api: true do end describe "POST /projects/:id/issues" do - it "creates a new project issue" do + it 'creates a new project issue' do post api("/projects/#{project.id}/issues", user), title: 'new issue', labels: 'label, label2' + expect(response).to have_http_status(201) expect(json_response['title']).to eq('new issue') expect(json_response['description']).to be_nil expect(json_response['labels']).to eq(['label', 'label2']) + expect(json_response['confidential']).to be_falsy + end + + it 'creates a new confidential project issue' do + post api("/projects/#{project.id}/issues", user), + title: 'new issue', confidential: true + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq('new issue') + expect(json_response['confidential']).to be_truthy + end + + it 'creates a new confidential project issue with a different param' do + post api("/projects/#{project.id}/issues", user), + title: 'new issue', confidential: 'y' + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq('new issue') + expect(json_response['confidential']).to be_truthy + end + + it 'creates a public issue when confidential param is false' do + post api("/projects/#{project.id}/issues", user), + title: 'new issue', confidential: false + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq('new issue') + expect(json_response['confidential']).to be_falsy + end + + it 'creates a public issue when confidential param is invalid' do + post api("/projects/#{project.id}/issues", user), + title: 'new issue', confidential: 'foo' + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq('new issue') + expect(json_response['confidential']).to be_falsy + end + + it "sends notifications for subscribers of newly added labels" do + label = project.labels.first + label.toggle_subscription(user2) + + perform_enqueued_jobs do + post api("/projects/#{project.id}/issues", user), + title: 'new issue', labels: label.title + end + + should_email(user2) end it "returns a 400 bad request if title not given" do @@ -619,6 +785,30 @@ describe API::API, api: true do expect(response).to have_http_status(200) expect(json_response['title']).to eq('updated title') end + + it 'sets an issue to confidential' do + put api("/projects/#{project.id}/issues/#{issue.id}", user), + confidential: true + + expect(response).to have_http_status(200) + expect(json_response['confidential']).to be_truthy + end + + it 'makes a confidential issue public' do + put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user), + confidential: false + + expect(response).to have_http_status(200) + expect(json_response['confidential']).to be_falsy + end + + it 'does not update a confidential issue with wrong confidential flag' do + put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user), + confidential: 'foo' + + expect(response).to have_http_status(200) + expect(json_response['confidential']).to be_truthy + end end end @@ -633,6 +823,18 @@ describe API::API, api: true do expect(json_response['labels']).to eq([label.title]) end + it "sends notifications for subscribers of newly added labels when issue is updated" do + label = create(:label, title: 'foo', color: '#FFAABB', project: project) + label.toggle_subscription(user2) + + perform_enqueued_jobs do + put api("/projects/#{project.id}/issues/#{issue.id}", user), + title: 'updated title', labels: label.title + end + + should_email(user2) + end + it 'removes all labels' do put api("/projects/#{project.id}/issues/#{issue.id}", user), labels: '' diff --git a/spec/requests/api/lint_spec.rb b/spec/requests/api/lint_spec.rb new file mode 100644 index 00000000000..391fc13a380 --- /dev/null +++ b/spec/requests/api/lint_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe API::Lint, api: true do + include ApiHelpers + + describe 'POST /ci/lint' do + context 'with valid .gitlab-ci.yaml content' do + let(:yaml_content) do + File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) + end + + it 'passes validation' do + post api('/ci/lint'), { content: yaml_content } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Hash + expect(json_response['status']).to eq('valid') + expect(json_response['errors']).to eq([]) + end + end + + context 'with an invalid .gitlab_ci.yml' do + it 'responds with errors about invalid syntax' do + post api('/ci/lint'), { content: 'invalid content' } + + expect(response).to have_http_status(200) + expect(json_response['status']).to eq('invalid') + expect(json_response['errors']).to eq(['Invalid configuration format']) + end + + it "responds with errors about invalid configuration" do + post api('/ci/lint'), { content: '{ image: "ruby:2.1", services: ["postgres"] }' } + + expect(response).to have_http_status(200) + expect(json_response['status']).to eq('invalid') + expect(json_response['errors']).to eq(['jobs config should contain at least one visible job']) + end + end + + context 'without the content parameter' do + it 'responds with validation error about missing content' do + post api('/ci/lint') + + expect(response).to have_http_status(400) + expect(json_response['error']).to eq('content is missing') + end + end + end +end diff --git a/spec/requests/api/merge_request_diffs_spec.rb b/spec/requests/api/merge_request_diffs_spec.rb new file mode 100644 index 00000000000..8f1e5ac9891 --- /dev/null +++ b/spec/requests/api/merge_request_diffs_spec.rb @@ -0,0 +1,49 @@ +require "spec_helper" + +describe API::API, 'MergeRequestDiffs', api: true do + include ApiHelpers + + let!(:user) { create(:user) } + let!(:merge_request) { create(:merge_request, importing: true) } + let!(:project) { merge_request.target_project } + + before do + merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') + merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') + project.team << [user, :master] + end + + describe 'GET /projects/:id/merge_requests/:merge_request_id/versions' do + context 'valid merge request' do + before { get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions", user) } + let(:merge_request_diff) { merge_request.merge_request_diffs.first } + + it { expect(response.status).to eq 200 } + it { expect(json_response.size).to eq(merge_request.merge_request_diffs.size) } + it { expect(json_response.first['id']).to eq(merge_request_diff.id) } + it { expect(json_response.first['head_commit_sha']).to eq(merge_request_diff.head_commit_sha) } + end + + it 'returns a 404 when merge_request_id not found' do + get api("/projects/#{project.id}/merge_requests/999/versions", user) + expect(response).to have_http_status(404) + end + end + + describe 'GET /projects/:id/merge_requests/:merge_request_id/versions/:version_id' do + context 'valid merge request' do + before { get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/#{merge_request_diff.id}", user) } + let(:merge_request_diff) { merge_request.merge_request_diffs.first } + + it { expect(response.status).to eq 200 } + it { expect(json_response['id']).to eq(merge_request_diff.id) } + it { expect(json_response['head_commit_sha']).to eq(merge_request_diff.head_commit_sha) } + it { expect(json_response['diffs'].size).to eq(merge_request_diff.diffs.size) } + end + + it 'returns a 404 when merge_request_id not found' do + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/999", user) + expect(response).to have_http_status(404) + end + end +end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index baff872e28e..a7930c59df9 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -9,7 +9,7 @@ describe API::API, api: true do let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) } let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) } - let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds) } + let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') } let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") } let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") } let(:milestone) { create(:milestone, title: '1.0.0', project: project) } @@ -34,6 +34,13 @@ describe API::API, api: true do expect(json_response.length).to eq(3) expect(json_response.last['title']).to eq(merge_request.title) expect(json_response.last).to have_key('web_url') + expect(json_response.last['sha']).to eq(merge_request.diff_head_sha) + expect(json_response.last['merge_commit_sha']).to be_nil + expect(json_response.last['merge_commit_sha']).to eq(merge_request.merge_commit_sha) + expect(json_response.first['title']).to eq(merge_request_merged.title) + expect(json_response.first['sha']).to eq(merge_request_merged.diff_head_sha) + expect(json_response.first['merge_commit_sha']).not_to be_nil + expect(json_response.first['merge_commit_sha']).to eq(merge_request_merged.merge_commit_sha) end it "returns an array of all merge_requests" do diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index 737fa14cbb0..223444ea39f 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -25,7 +25,7 @@ describe API::API, api: true do let!(:cross_reference_note) do create :note, noteable: ext_issue, project: ext_proj, - note: "mentioned in issue #{private_issue.to_reference(ext_proj)}", + note: "Mentioned in issue #{private_issue.to_reference(ext_proj)}", system: true end diff --git a/spec/requests/api/notification_settings_spec.rb b/spec/requests/api/notification_settings_spec.rb new file mode 100644 index 00000000000..e6d8a5ee954 --- /dev/null +++ b/spec/requests/api/notification_settings_spec.rb @@ -0,0 +1,89 @@ +require 'spec_helper' + +describe API::API, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let!(:group) { create(:group) } + let!(:project) { create(:project, :public, creator_id: user.id, namespace: group) } + + describe "GET /notification_settings" do + it "returns global notification settings for the current user" do + get api("/notification_settings", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_a Hash + expect(json_response['notification_email']).to eq(user.notification_email) + expect(json_response['level']).to eq(user.global_notification_setting.level) + end + end + + describe "PUT /notification_settings" do + let(:email) { create(:email, user: user) } + + it "updates global notification settings for the current user" do + put api("/notification_settings", user), { level: 'watch', notification_email: email.email } + + expect(response).to have_http_status(200) + expect(json_response['notification_email']).to eq(email.email) + expect(user.reload.notification_email).to eq(email.email) + expect(json_response['level']).to eq(user.reload.global_notification_setting.level) + end + end + + describe "PUT /notification_settings" do + it "fails on non-user email address" do + put api("/notification_settings", user), { notification_email: 'invalid@example.com' } + + expect(response).to have_http_status(400) + end + end + + describe "GET /groups/:id/notification_settings" do + it "returns group level notification settings for the current user" do + get api("/groups/#{group.id}/notification_settings", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_a Hash + expect(json_response['level']).to eq(user.notification_settings_for(group).level) + end + end + + describe "PUT /groups/:id/notification_settings" do + it "updates group level notification settings for the current user" do + put api("/groups/#{group.id}/notification_settings", user), { level: 'watch' } + + expect(response).to have_http_status(200) + expect(json_response['level']).to eq(user.reload.notification_settings_for(group).level) + end + end + + describe "GET /projects/:id/notification_settings" do + it "returns project level notification settings for the current user" do + get api("/projects/#{project.id}/notification_settings", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_a Hash + expect(json_response['level']).to eq(user.notification_settings_for(project).level) + end + end + + describe "PUT /projects/:id/notification_settings" do + it "updates project level notification settings for the current user" do + put api("/projects/#{project.id}/notification_settings", user), { level: 'custom', new_note: true } + + expect(response).to have_http_status(200) + expect(json_response['level']).to eq(user.reload.notification_settings_for(project).level) + expect(json_response['events']['new_note']).to eq(true) + expect(json_response['events']['new_issue']).to eq(false) + end + end + + describe "PUT /projects/:id/notification_settings" do + it "fails on invalid level" do + put api("/projects/#{project.id}/notification_settings", user), { level: 'invalid' } + + expect(response).to have_http_status(400) + end + end +end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 63f2467be63..28aa56e8644 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -73,7 +73,7 @@ describe API::API, api: true do end it 'does not include open_issues_count' do - project.update_attributes( { issues_enabled: false } ) + project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) get api('/projects', user) expect(response.status).to eq 200 @@ -231,8 +231,15 @@ describe API::API, api: true do post api('/projects', user), project project.each_pair do |k, v| + next if %i{ issues_enabled merge_requests_enabled wiki_enabled }.include?(k) expect(json_response[k.to_s]).to eq(v) end + + # Check feature permissions attributes + project = Project.find_by_path(project[:path]) + expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED) + expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::DISABLED) + expect(project.project_feature.wiki_access_level).to eq(ProjectFeature::DISABLED) end it 'sets a project as public' do diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 0bbba64a6d5..ef73778efa9 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -605,6 +605,7 @@ describe API::API, api: true do expect(json_response['can_create_project']).to eq(user.can_create_project?) expect(json_response['can_create_group']).to eq(user.can_create_group?) expect(json_response['projects_limit']).to eq(user.projects_limit) + expect(json_response['private_token']).to be_blank end it "returns 401 error if user is unauthenticated" do diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index ca7932dc5da..9e390bea50b 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -230,7 +230,8 @@ describe Ci::API::API do let(:post_url) { ci_api("/builds/#{build.id}/artifacts") } let(:delete_url) { ci_api("/builds/#{build.id}/artifacts") } let(:get_url) { ci_api("/builds/#{build.id}/artifacts") } - let(:headers) { { "GitLab-Workhorse" => "1.0" } } + let(:jwt_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') } + let(:headers) { { "GitLab-Workhorse" => "1.0", Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => jwt_token } } let(:headers_with_token) { headers.merge(Ci::API::Helpers::BUILD_TOKEN_HEADER => build.token) } before { build.run! } @@ -240,14 +241,22 @@ describe Ci::API::API do it "using token as parameter" do post authorize_url, { token: build.token }, headers expect(response).to have_http_status(200) + expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) expect(json_response["TempPath"]).not_to be_nil end it "using token as header" do post authorize_url, {}, headers_with_token expect(response).to have_http_status(200) + expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) expect(json_response["TempPath"]).not_to be_nil end + + it "reject requests that did not go through gitlab-workhorse" do + headers.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER) + post authorize_url, { token: build.token }, headers + expect(response).to have_http_status(500) + end end context "should fail to post too large artifact" do diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index afaf4b7cefb..b7001fede40 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -1,6 +1,8 @@ require "spec_helper" describe 'Git HTTP requests', lib: true do + include WorkhorseHelpers + let(:user) { create(:user) } let(:project) { create(:project, path: 'project.git-project') } @@ -48,6 +50,7 @@ describe 'Git HTTP requests', lib: true do expect(response).to have_http_status(200) expect(json_body['RepoPath']).to include(wiki.repository.path_with_namespace) + expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) end end end @@ -63,6 +66,7 @@ describe 'Git HTTP requests', lib: true do it "downloads get status 200" do download(path, {}) do |response| expect(response).to have_http_status(200) + expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) end end @@ -101,6 +105,14 @@ describe 'Git HTTP requests', lib: true do end end end + + context 'when the request is not from gitlab-workhorse' do + it 'raises an exception' do + expect do + get("/#{project.path_with_namespace}.git/info/refs?service=git-upload-pack") + end.to raise_error(JWT::DecodeError) + end + end end context "when the project is private" do @@ -170,11 +182,13 @@ describe 'Git HTTP requests', lib: true do clone_get(path, env) expect(response).to have_http_status(200) + expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) end it "uploads get status 200" do upload(path, env) do |response| expect(response).to have_http_status(200) + expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) end end end @@ -189,6 +203,7 @@ describe 'Git HTTP requests', lib: true do clone_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token expect(response).to have_http_status(200) + expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) end it "uploads get status 401 (no project existence information leak)" do @@ -289,13 +304,15 @@ describe 'Git HTTP requests', lib: true do let(:project) { FactoryGirl.create :empty_project } before do - project.update_attributes(runners_token: token, builds_enabled: true) + project.update_attributes(runners_token: token) + project.project_feature.update_attributes(builds_access_level: ProjectFeature::ENABLED) end it "downloads get status 200" do clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: token expect(response).to have_http_status(200) + expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) end it "uploads get status 401 (no project existence information leak)" do @@ -425,7 +442,7 @@ describe 'Git HTTP requests', lib: true do end def auth_env(user, password, spnego_request_token) - env = {} + env = workhorse_internal_api_request_header if user && password env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(user, password) elsif spnego_request_token diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb index c6172b9cc7d..fc42b534dca 100644 --- a/spec/requests/jwt_controller_spec.rb +++ b/spec/requests/jwt_controller_spec.rb @@ -22,19 +22,20 @@ describe JwtController do context 'when using authorized request' do context 'using CI token' do - let(:project) { create(:empty_project, runners_token: 'token', builds_enabled: builds_enabled) } + let(:project) { create(:empty_project, runners_token: 'token') } let(:headers) { { authorization: credentials('gitlab-ci-token', project.runners_token) } } - subject! { get '/jwt/auth', parameters, headers } - context 'project with enabled CI' do - let(:builds_enabled) { true } - + subject! { get '/jwt/auth', parameters, headers } it { expect(service_class).to have_received(:new).with(project, nil, parameters) } end context 'project with disabled CI' do - let(:builds_enabled) { false } + before do + project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED) + end + + subject! { get '/jwt/auth', parameters, headers } it { expect(response).to have_http_status(403) } end diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb index 4c9b4a8ba42..6e551bb65fa 100644 --- a/spec/requests/lfs_http_spec.rb +++ b/spec/requests/lfs_http_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe 'Git LFS API and storage' do + include WorkhorseHelpers + let(:user) { create(:user) } let!(:lfs_object) { create(:lfs_object, :with_file) } @@ -44,6 +46,113 @@ describe 'Git LFS API and storage' do end end + context 'project specific LFS settings' do + let(:project) { create(:empty_project) } + let(:body) do + { + 'objects' => [ + { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', + 'size' => 1575078 + }, + { 'oid' => sample_oid, + 'size' => sample_size + } + ], + 'operation' => 'upload' + } + end + let(:authorization) { authorize_user } + + context 'with LFS disabled globally' do + before do + project.team << [user, :master] + allow(Gitlab.config.lfs).to receive(:enabled).and_return(false) + end + + describe 'LFS disabled in project' do + before do + project.update_attribute(:lfs_enabled, false) + end + + it 'responds with a 501 message on upload' do + post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers + + expect(response).to have_http_status(501) + end + + it 'responds with a 501 message on download' do + get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", nil, headers + + expect(response).to have_http_status(501) + end + end + + describe 'LFS enabled in project' do + before do + project.update_attribute(:lfs_enabled, true) + end + + it 'responds with a 501 message on upload' do + post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers + + expect(response).to have_http_status(501) + end + + it 'responds with a 501 message on download' do + get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", nil, headers + + expect(response).to have_http_status(501) + end + end + end + + context 'with LFS enabled globally' do + before do + project.team << [user, :master] + enable_lfs + end + + describe 'LFS disabled in project' do + before do + project.update_attribute(:lfs_enabled, false) + end + + it 'responds with a 403 message on upload' do + post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers + + expect(response).to have_http_status(403) + expect(json_response).to include('message' => 'Access forbidden. Check your access level.') + end + + it 'responds with a 403 message on download' do + get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", nil, headers + + expect(response).to have_http_status(403) + expect(json_response).to include('message' => 'Access forbidden. Check your access level.') + end + end + + describe 'LFS enabled in project' do + before do + project.update_attribute(:lfs_enabled, true) + end + + it 'responds with a 200 message on upload' do + post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers + + expect(response).to have_http_status(200) + expect(json_response['objects'].first['size']).to eq(1575078) + end + + it 'responds with a 200 message on download' do + get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", nil, headers + + expect(response).to have_http_status(200) + end + end + end + end + describe 'deprecated API' do let(:project) { create(:empty_project) } @@ -608,6 +717,12 @@ describe 'Git LFS API and storage' do project.team << [user, :developer] end + context 'and the request bypassed workhorse' do + it 'raises an exception' do + expect { put_authorize(verified: false) }.to raise_error JWT::DecodeError + end + end + context 'and request is sent by gitlab-workhorse to authorize the request' do before do put_authorize @@ -617,6 +732,10 @@ describe 'Git LFS API and storage' do expect(response).to have_http_status(200) end + it 'uses the gitlab-workhorse content type' do + expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + end + it 'responds with status 200, location of lfs store and object details' do expect(json_response['StoreLFSPath']).to eq("#{Gitlab.config.shared.path}/lfs-objects/tmp/upload") expect(json_response['LfsOid']).to eq(sample_oid) @@ -756,8 +875,11 @@ describe 'Git LFS API and storage' do end end - def put_authorize - put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}/authorize", nil, headers + def put_authorize(verified: true) + authorize_headers = headers + authorize_headers.merge!(workhorse_internal_api_request_header) if verified + + put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}/authorize", nil, authorize_headers end def put_finalize(lfs_tmp = lfs_tmp_file) diff --git a/spec/requests/projects/artifacts_controller_spec.rb b/spec/requests/projects/artifacts_controller_spec.rb new file mode 100644 index 00000000000..e02f0eacc93 --- /dev/null +++ b/spec/requests/projects/artifacts_controller_spec.rb @@ -0,0 +1,117 @@ +require 'spec_helper' + +describe Projects::ArtifactsController do + let(:user) { create(:user) } + let(:project) { create(:project) } + + let(:pipeline) do + create(:ci_pipeline, + project: project, + sha: project.commit.sha, + ref: project.default_branch, + status: 'success') + end + + let(:build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } + + describe 'GET /:project/builds/artifacts/:ref_name/browse?job=name' do + before do + project.team << [user, :developer] + + login_as(user) + end + + def path_from_ref( + ref = pipeline.ref, job = build.name, path = 'browse') + latest_succeeded_namespace_project_artifacts_path( + project.namespace, + project, + [ref, path].join('/'), + job: job) + end + + context 'cannot find the build' do + shared_examples 'not found' do + it { expect(response).to have_http_status(:not_found) } + end + + context 'has no such ref' do + before do + get path_from_ref('TAIL', build.name) + end + + it_behaves_like 'not found' + end + + context 'has no such build' do + before do + get path_from_ref(pipeline.ref, 'NOBUILD') + end + + it_behaves_like 'not found' + end + + context 'has no path' do + before do + get path_from_ref(pipeline.sha, build.name, '') + end + + it_behaves_like 'not found' + end + end + + context 'found the build and redirect' do + shared_examples 'redirect to the build' do + it 'redirects' do + path = browse_namespace_project_build_artifacts_path( + project.namespace, + project, + build) + + expect(response).to redirect_to(path) + end + end + + context 'with regular branch' do + before do + pipeline.update(ref: 'master', + sha: project.commit('master').sha) + + get path_from_ref('master') + end + + it_behaves_like 'redirect to the build' + end + + context 'with branch name containing slash' do + before do + pipeline.update(ref: 'improve/awesome', + sha: project.commit('improve/awesome').sha) + + get path_from_ref('improve/awesome') + end + + it_behaves_like 'redirect to the build' + end + + context 'with branch name and path containing slashes' do + before do + pipeline.update(ref: 'improve/awesome', + sha: project.commit('improve/awesome').sha) + + get path_from_ref('improve/awesome', build.name, 'file/README.md') + end + + it 'redirects' do + path = file_namespace_project_build_artifacts_path( + project.namespace, + project, + build, + 'README.md') + + expect(response).to redirect_to(path) + end + end + end + end +end diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index d65648dd0b2..4bc3cddd9c2 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -107,9 +107,9 @@ describe HelpController, "routing" do end it 'to #show' do - path = '/help/markdown/markdown.md' + path = '/help/user/markdown.md' expect(get(path)).to route_to('help#show', - path: 'markdown/markdown', + path: 'user/markdown', format: 'md') path = '/help/workflow/protected_branches/protected_branches1.png' diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb index f7f45983d26..cf4c5f13635 100644 --- a/spec/services/boards/issues/list_service_spec.rb +++ b/spec/services/boards/issues/list_service_spec.rb @@ -30,7 +30,7 @@ describe Boards::Issues::ListService, services: true do let!(:closed_issue1) { create(:labeled_issue, :closed, project: project, labels: [bug]) } let!(:closed_issue2) { create(:labeled_issue, :closed, project: project, labels: [p3]) } let!(:closed_issue3) { create(:issue, :closed, project: project) } - let!(:closed_issue4) { create(:labeled_issue, :closed, project: project, labels: [p1]) } + let!(:closed_issue4) { create(:labeled_issue, :closed, project: project, labels: [p1, development]) } before do project.team << [user, :developer] @@ -58,15 +58,15 @@ describe Boards::Issues::ListService, services: true do issues = described_class.new(project, user, params).execute - expect(issues).to eq [closed_issue4, closed_issue2, closed_issue3, closed_issue1] + expect(issues).to eq [closed_issue2, closed_issue3, closed_issue1] end - it 'returns opened issues that have label list applied when listing issues from a label list' do + it 'returns opened/closed issues that have label list applied when listing issues from a label list' do params = { id: list1.id } issues = described_class.new(project, user, params).execute - expect(issues).to eq [list1_issue3, list1_issue1, list1_issue2] + expect(issues).to eq [closed_issue4, list1_issue3, list1_issue1, list1_issue2] end end end diff --git a/spec/services/boards/lists/create_service_spec.rb b/spec/services/boards/lists/create_service_spec.rb index 5e7e145065e..90764b86b16 100644 --- a/spec/services/boards/lists/create_service_spec.rb +++ b/spec/services/boards/lists/create_service_spec.rb @@ -5,7 +5,7 @@ describe Boards::Lists::CreateService, services: true do let(:project) { create(:project_with_board) } let(:board) { project.board } let(:user) { create(:user) } - let(:label) { create(:label, name: 'in-progress') } + let(:label) { create(:label, project: project, name: 'in-progress') } subject(:service) { described_class.new(project, user, label_id: label.id) } @@ -50,5 +50,14 @@ describe Boards::Lists::CreateService, services: true do expect(list2.reload.position).to eq 1 end end + + context 'when provided label does not belongs to the project' do + it 'raises an error' do + label = create(:label, name: 'in-development') + service = described_class.new(project, user, label_id: label.id) + + expect { service.execute }.to raise_error(ActiveRecord::RecordNotFound) + end + end end end diff --git a/spec/services/ci/image_for_build_service_spec.rb b/spec/services/ci/image_for_build_service_spec.rb index c931c3e4829..b3e0a7b9b58 100644 --- a/spec/services/ci/image_for_build_service_spec.rb +++ b/spec/services/ci/image_for_build_service_spec.rb @@ -5,7 +5,7 @@ module Ci let(:service) { ImageForBuildService.new } let(:project) { FactoryGirl.create(:empty_project) } let(:commit_sha) { '01234567890123456789' } - let(:pipeline) { project.ensure_pipeline(commit_sha, 'master') } + let(:pipeline) { project.ensure_pipeline('master', commit_sha) } let(:build) { FactoryGirl.create(:ci_build, pipeline: pipeline) } describe '#execute' do diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_build_service_spec.rb index 026d0ca6534..1e21a32a062 100644 --- a/spec/services/ci/register_build_service_spec.rb +++ b/spec/services/ci/register_build_service_spec.rb @@ -151,6 +151,25 @@ module Ci it { expect(build.runner).to eq(specific_runner) } end end + + context 'disallow when builds are disabled' do + before do + project.update(shared_runners_enabled: true) + project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED) + end + + context 'and uses shared runner' do + let(:build) { service.execute(shared_runner) } + + it { expect(build).to be_nil } + end + + context 'and uses specific runner' do + let(:build) { service.execute(specific_runner) } + + it { expect(build).to be_nil } + end + end end end end diff --git a/spec/services/issues/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb index ac08aa53b0b..6f7ce8ca992 100644 --- a/spec/services/issues/bulk_update_service_spec.rb +++ b/spec/services/issuable/bulk_update_service_spec.rb @@ -1,14 +1,14 @@ require 'spec_helper' -describe Issues::BulkUpdateService, services: true do +describe Issuable::BulkUpdateService, services: true do let(:user) { create(:user) } let(:project) { create(:empty_project, namespace: user.namespace) } def bulk_update(issues, extra_params = {}) bulk_update_params = extra_params - .reverse_merge(issues_ids: Array(issues).map(&:id).join(',')) + .reverse_merge(issuable_ids: Array(issues).map(&:id).join(',')) - Issues::BulkUpdateService.new(project, user, bulk_update_params).execute + Issuable::BulkUpdateService.new(project, user, bulk_update_params).execute('issue') end describe 'close issues' do diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb index aff022a573e..5dfb33f4b28 100644 --- a/spec/services/issues/close_service_spec.rb +++ b/spec/services/issues/close_service_spec.rb @@ -18,12 +18,12 @@ describe Issues::CloseService, services: true do context "valid params" do before do perform_enqueued_jobs do - @issue = described_class.new(project, user, {}).execute(issue) + described_class.new(project, user).execute(issue) end end - it { expect(@issue).to be_valid } - it { expect(@issue).to be_closed } + it { expect(issue).to be_valid } + it { expect(issue).to be_closed } it 'sends email to user2 about assign of new issue' do email = ActionMailer::Base.deliveries.last @@ -32,7 +32,7 @@ describe Issues::CloseService, services: true do end it 'creates system note about issue reassign' do - note = @issue.notes.last + note = issue.notes.last expect(note.note).to include "Status changed to closed" end @@ -44,23 +44,43 @@ describe Issues::CloseService, services: true do context 'current user is not authorized to close issue' do before do perform_enqueued_jobs do - @issue = described_class.new(project, guest).execute(issue) + described_class.new(project, guest).execute(issue) end end it 'does not close the issue' do - expect(@issue).to be_open + expect(issue).to be_open end end - context "external issue tracker" do + context 'when issue is not confidential' do + it 'executes issue hooks' do + expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :issue_hooks) + expect(project).to receive(:execute_services).with(an_instance_of(Hash), :issue_hooks) + + described_class.new(project, user).execute(issue) + end + end + + context 'when issue is confidential' do + it 'executes confidential issue hooks' do + issue = create(:issue, :confidential, project: project) + + expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :confidential_issue_hooks) + expect(project).to receive(:execute_services).with(an_instance_of(Hash), :confidential_issue_hooks) + + described_class.new(project, user).execute(issue) + end + end + + context 'external issue tracker' do before do allow(project).to receive(:default_issues_tracker?).and_return(false) - @issue = described_class.new(project, user, {}).execute(issue) + described_class.new(project, user).execute(issue) end - it { expect(@issue).to be_valid } - it { expect(@issue).to be_opened } + it { expect(issue).to be_valid } + it { expect(issue).to be_opened } it { expect(todo.reload).to be_pending } end end diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index fcc3c0a00bd..58569ba96c3 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -72,6 +72,24 @@ describe Issues::CreateService, services: true do expect(issue.milestone).not_to eq milestone end end + + it 'executes issue hooks when issue is not confidential' do + opts = { title: 'Title', description: 'Description', confidential: false } + + expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :issue_hooks) + expect(project).to receive(:execute_services).with(an_instance_of(Hash), :issue_hooks) + + described_class.new(project, user, opts).execute + end + + it 'executes confidential issue hooks when issue is confidential' do + opts = { title: 'Title', description: 'Description', confidential: true } + + expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :confidential_issue_hooks) + expect(project).to receive(:execute_services).with(an_instance_of(Hash), :confidential_issue_hooks) + + described_class.new(project, user, opts).execute + end end it_behaves_like 'new issuable record that supports slash commands' diff --git a/spec/services/issues/reopen_service_spec.rb b/spec/services/issues/reopen_service_spec.rb index 34a89fcd4e1..93a8270fd16 100644 --- a/spec/services/issues/reopen_service_spec.rb +++ b/spec/services/issues/reopen_service_spec.rb @@ -1,24 +1,50 @@ require 'spec_helper' describe Issues::ReopenService, services: true do - let(:guest) { create(:user) } - let(:issue) { create(:issue, :closed) } - let(:project) { issue.project } - - before do - project.team << [guest, :guest] - end + let(:project) { create(:empty_project) } + let(:issue) { create(:issue, :closed, project: project) } describe '#execute' do - context 'current user is not authorized to reopen issue' do + context 'when user is not authorized to reopen issue' do before do + guest = create(:user) + project.team << [guest, :guest] + perform_enqueued_jobs do - @issue = described_class.new(project, guest).execute(issue) + described_class.new(project, guest).execute(issue) end end it 'does not reopen the issue' do - expect(@issue).to be_closed + expect(issue).to be_closed + end + end + + context 'when user is authrized to reopen issue' do + let(:user) { create(:user) } + + before do + project.team << [user, :master] + end + + context 'when issue is not confidential' do + it 'executes issue hooks' do + expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :issue_hooks) + expect(project).to receive(:execute_services).with(an_instance_of(Hash), :issue_hooks) + + described_class.new(project, user).execute(issue) + end + end + + context 'when issue is confidential' do + it 'executes confidential issue hooks' do + issue = create(:issue, :confidential, :closed, project: project) + + expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :confidential_issue_hooks) + expect(project).to receive(:execute_services).with(an_instance_of(Hash), :confidential_issue_hooks) + + described_class.new(project, user).execute(issue) + end end end end diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 0313f424463..4f5375a3583 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -23,11 +23,15 @@ describe Issues::UpdateService, services: true do describe 'execute' do def find_note(starting_with) - @issue.notes.find do |note| + issue.notes.find do |note| note && note.note.start_with?(starting_with) end end + def update_issue(opts) + described_class.new(project, user, opts).execute(issue) + end + context "valid params" do before do opts = { @@ -35,23 +39,20 @@ describe Issues::UpdateService, services: true do description: 'Also please fix', assignee_id: user2.id, state_event: 'close', - label_ids: [label.id], - confidential: true + label_ids: [label.id] } perform_enqueued_jobs do - @issue = Issues::UpdateService.new(project, user, opts).execute(issue) + update_issue(opts) end - - @issue.reload end - it { expect(@issue).to be_valid } - it { expect(@issue.title).to eq('New title') } - it { expect(@issue.assignee).to eq(user2) } - it { expect(@issue).to be_closed } - it { expect(@issue.labels.count).to eq(1) } - it { expect(@issue.labels.first.title).to eq(label.name) } + it { expect(issue).to be_valid } + it { expect(issue.title).to eq('New title') } + it { expect(issue.assignee).to eq(user2) } + it { expect(issue).to be_closed } + it { expect(issue.labels.count).to eq(1) } + it { expect(issue.labels.first.title).to eq(label.name) } it 'sends email to user2 about assign of new issue and email to user3 about issue unassignment' do deliveries = ActionMailer::Base.deliveries @@ -81,18 +82,35 @@ describe Issues::UpdateService, services: true do expect(note).not_to be_nil expect(note.note).to eq 'Changed title: **{-Old-} title** → **{+New+} title**' end + end + + context 'when issue turns confidential' do + let(:opts) do + { + title: 'New title', + description: 'Also please fix', + assignee_id: user2.id, + state_event: 'close', + label_ids: [label.id], + confidential: true + } + end it 'creates system note about confidentiality change' do + update_issue(confidential: true) + note = find_note('Made the issue confidential') expect(note).not_to be_nil expect(note.note).to eq 'Made the issue confidential' end - end - def update_issue(opts) - @issue = Issues::UpdateService.new(project, user, opts).execute(issue) - @issue.reload + it 'executes confidential issue hooks' do + expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :confidential_issue_hooks) + expect(project).to receive(:execute_services).with(an_instance_of(Hash), :confidential_issue_hooks) + + update_issue(confidential: true) + end end context 'todos' do @@ -100,7 +118,7 @@ describe Issues::UpdateService, services: true do context 'when the title change' do before do - update_issue({ title: 'New title' }) + update_issue(title: 'New title') end it 'marks pending todos as done' do @@ -110,7 +128,7 @@ describe Issues::UpdateService, services: true do context 'when the description change' do before do - update_issue({ description: 'Also please fix' }) + update_issue(description: 'Also please fix') end it 'marks todos as done' do @@ -120,7 +138,7 @@ describe Issues::UpdateService, services: true do context 'when is reassigned' do before do - update_issue({ assignee: user2 }) + update_issue(assignee: user2) end it 'marks previous assignee todos as done' do @@ -144,7 +162,7 @@ describe Issues::UpdateService, services: true do context 'when the milestone change' do before do - update_issue({ milestone: create(:milestone) }) + update_issue(milestone: create(:milestone)) end it 'marks todos as done' do @@ -154,7 +172,7 @@ describe Issues::UpdateService, services: true do context 'when the labels change' do before do - update_issue({ label_ids: [label.id] }) + update_issue(label_ids: [label.id]) end it 'marks todos as done' do @@ -165,6 +183,7 @@ describe Issues::UpdateService, services: true do context 'when the issue is relabeled' do let!(:non_subscriber) { create(:user) } + let!(:subscriber) do create(:user).tap do |u| label.toggle_subscription(u) @@ -176,7 +195,7 @@ describe Issues::UpdateService, services: true do opts = { label_ids: [label.id] } perform_enqueued_jobs do - @issue = Issues::UpdateService.new(project, user, opts).execute(issue) + @issue = described_class.new(project, user, opts).execute(issue) end should_email(subscriber) @@ -190,7 +209,7 @@ describe Issues::UpdateService, services: true do opts = { label_ids: [label.id, label2.id] } perform_enqueued_jobs do - @issue = Issues::UpdateService.new(project, user, opts).execute(issue) + @issue = described_class.new(project, user, opts).execute(issue) end should_not_email(subscriber) @@ -201,7 +220,7 @@ describe Issues::UpdateService, services: true do opts = { label_ids: [label2.id] } perform_enqueued_jobs do - @issue = Issues::UpdateService.new(project, user, opts).execute(issue) + @issue = described_class.new(project, user, opts).execute(issue) end should_not_email(subscriber) @@ -210,13 +229,15 @@ describe Issues::UpdateService, services: true do end end - context 'when Issue has tasks' do - before { update_issue({ description: "- [ ] Task 1\n- [ ] Task 2" }) } + context 'when issue has tasks' do + before do + update_issue(description: "- [ ] Task 1\n- [ ] Task 2") + end - it { expect(@issue.tasks?).to eq(true) } + it { expect(issue.tasks?).to eq(true) } context 'when tasks are marked as completed' do - before { update_issue({ description: "- [x] Task 1\n- [X] Task 2" }) } + before { update_issue(description: "- [x] Task 1\n- [X] Task 2") } it 'creates system note about task status change' do note1 = find_note('Marked the task **Task 1** as completed') @@ -229,8 +250,8 @@ describe Issues::UpdateService, services: true do context 'when tasks are marked as incomplete' do before do - update_issue({ description: "- [x] Task 1\n- [X] Task 2" }) - update_issue({ description: "- [ ] Task 1\n- [ ] Task 2" }) + update_issue(description: "- [x] Task 1\n- [X] Task 2") + update_issue(description: "- [ ] Task 1\n- [ ] Task 2") end it 'creates system note about task status change' do @@ -244,8 +265,8 @@ describe Issues::UpdateService, services: true do context 'when tasks position has been modified' do before do - update_issue({ description: "- [x] Task 1\n- [X] Task 2" }) - update_issue({ description: "- [x] Task 1\n- [ ] Task 3\n- [ ] Task 2" }) + update_issue(description: "- [x] Task 1\n- [X] Task 2") + update_issue(description: "- [x] Task 1\n- [ ] Task 3\n- [ ] Task 2") end it 'does not create a system note' do @@ -257,8 +278,8 @@ describe Issues::UpdateService, services: true do context 'when a Task list with a completed item is totally replaced' do before do - update_issue({ description: "- [ ] Task 1\n- [X] Task 2" }) - update_issue({ description: "- [ ] One\n- [ ] Two\n- [ ] Three" }) + update_issue(description: "- [ ] Task 1\n- [X] Task 2") + update_issue(description: "- [ ] One\n- [ ] Two\n- [ ] Three") end it 'does not create a system note referencing the position the old item' do @@ -269,7 +290,7 @@ describe Issues::UpdateService, services: true do it 'does not generate a new note at all' do expect do - update_issue({ description: "- [ ] One\n- [ ] Two\n- [ ] Three" }) + update_issue(description: "- [ ] One\n- [ ] Two\n- [ ] Three") end.not_to change { Note.count } end end @@ -277,7 +298,7 @@ describe Issues::UpdateService, services: true do context 'updating labels' do let(:label3) { create(:label, project: project) } - let(:result) { Issues::UpdateService.new(project, user, params).execute(issue).reload } + let(:result) { described_class.new(project, user, params).execute(issue).reload } context 'when add_label_ids and label_ids are passed' do let(:params) { { label_ids: [label.id], add_label_ids: [label3.id] } } diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb index 232508cda23..0d586e2216b 100644 --- a/spec/services/merge_requests/build_service_spec.rb +++ b/spec/services/merge_requests/build_service_spec.rb @@ -99,14 +99,14 @@ describe MergeRequests::BuildService, services: true do let(:source_branch) { "#{issue.iid}-fix-issue" } it 'appends "Closes #$issue-iid" to the description' do - expect(merge_request.description).to eq("#{commit_1.safe_message.split(/\n+/, 2).last}\nCloses ##{issue.iid}") + expect(merge_request.description).to eq("#{commit_1.safe_message.split(/\n+/, 2).last}\n\nCloses ##{issue.iid}") end context 'merge request already has a description set' do let(:description) { 'Merge request description' } it 'appends "Closes #$issue-iid" to the description' do - expect(merge_request.description).to eq("#{description}\nCloses ##{issue.iid}") + expect(merge_request.description).to eq("#{description}\n\nCloses ##{issue.iid}") end end diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb index 8a4b76367e3..3a71776e81f 100644 --- a/spec/services/merge_requests/get_urls_service_spec.rb +++ b/spec/services/merge_requests/get_urls_service_spec.rb @@ -50,7 +50,7 @@ describe MergeRequests::GetUrlsService do let(:changes) { new_branch_changes } before do - project.merge_requests_enabled = false + project.project_feature.update_attribute(:merge_requests_access_level, ProjectFeature::DISABLED) end it_behaves_like 'no_merge_request_url' diff --git a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb index c4b87468275..807f89e80b7 100644 --- a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb +++ b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb @@ -6,7 +6,7 @@ describe MergeRequests::MergeRequestDiffCacheService do describe '#execute' do it 'retrieves the diff files to cache the highlighted result' do merge_request = create(:merge_request) - cache_key = [merge_request.merge_request_diff, 'highlighted-diff-files', Gitlab::Diff::FileCollection::MergeRequest.default_options] + cache_key = [merge_request.merge_request_diff, 'highlighted-diff-files', Gitlab::Diff::FileCollection::MergeRequestDiff.default_options] expect(Rails.cache).to receive(:read).with(cache_key).and_return({}) expect(Rails.cache).to receive(:write).with(cache_key, anything) diff --git a/spec/services/merge_requests/resolve_service_spec.rb b/spec/services/merge_requests/resolve_service_spec.rb new file mode 100644 index 00000000000..d71932458fa --- /dev/null +++ b/spec/services/merge_requests/resolve_service_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper' + +describe MergeRequests::ResolveService do + let(:user) { create(:user) } + let(:project) { create(:project) } + + 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 + end + + let(:merge_request) do + create(:merge_request, + source_branch: 'conflict-resolvable', source_project: project, + target_branch: 'conflict-start') + end + + let(:merge_request_from_fork) do + create(:merge_request, + source_branch: 'conflict-resolvable-fork', source_project: fork_project, + target_branch: 'conflict-start', target_project: project) + end + + describe '#execute' do + context 'with valid params' do + let(:params) do + { + sections: { + '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin' + }, + commit_message: 'This is a commit message!' + } + end + + context 'when the source and target project are the same' do + before do + MergeRequests::ResolveService.new(project, user, params).execute(merge_request) + end + + it 'creates a commit with the message' do + expect(merge_request.source_branch_head.message).to eq(params[:commit_message]) + end + + it 'creates a commit with the correct parents' do + expect(merge_request.source_branch_head.parents.map(&:id)). + to eq(['1450cd639e0bc6721eb02800169e464f212cde06', + '75284c70dd26c87f2a3fb65fd5a1f0b0138d3a6b']) + end + end + + context 'when the source project is a fork and does not contain the HEAD of the target branch' do + let!(:target_head) do + project.repository.commit_file(user, 'new-file-in-target', '', 'Add new file in target', 'conflict-start', false) + end + + before do + MergeRequests::ResolveService.new(fork_project, user, params).execute(merge_request_from_fork) + end + + it 'creates a commit with the message' do + expect(merge_request_from_fork.source_branch_head.message).to eq(params[:commit_message]) + end + + it 'creates a commit with the correct parents' do + expect(merge_request_from_fork.source_branch_head.parents.map(&:id)). + to eq(['404fa3fc7c2c9b5dacff102f353bdf55b1be2813', + target_head]) + end + end + end + + context 'when a resolution is missing' do + let(:invalid_params) { { sections: { '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head' } } } + let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) } + + it 'raises a MissingResolution error' do + expect { service.execute(merge_request) }. + to raise_error(Gitlab::Conflict::File::MissingResolution) + end + end + end +end diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index bbced59ff02..3ea1273abc3 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -69,7 +69,7 @@ describe Projects::CreateService, services: true do context 'wiki_enabled false does not create wiki repository directory' do before do - @opts.merge!(wiki_enabled: false) + @opts.merge!( { project_feature_attributes: { wiki_access_level: ProjectFeature::DISABLED } }) @project = create_project(@user, @opts) @path = ProjectWiki.new(@project, @user).send(:path_to_repo) end @@ -85,7 +85,7 @@ describe Projects::CreateService, services: true do context 'global builds_enabled false does not enable CI by default' do before do - @opts.merge!(builds_enabled: false) + project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED) end it { is_expected.to be_falsey } @@ -93,7 +93,7 @@ describe Projects::CreateService, services: true do context 'global builds_enabled true does enable CI by default' do before do - @opts.merge!(builds_enabled: true) + project.project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED) end it { is_expected.to be_truthy } diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 00427d6db2a..3d854a959f3 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -330,13 +330,13 @@ describe SystemNoteService, services: true do let(:mentioner) { project2.repository.commit } it 'references the mentioning commit' do - expect(subject.note).to eq "mentioned in commit #{mentioner.to_reference(project)}" + expect(subject.note).to eq "Mentioned in commit #{mentioner.to_reference(project)}" end end context 'from non-Commit' do it 'references the mentioning object' do - expect(subject.note).to eq "mentioned in issue #{mentioner.to_reference(project)}" + expect(subject.note).to eq "Mentioned in issue #{mentioner.to_reference(project)}" end end end @@ -346,13 +346,13 @@ describe SystemNoteService, services: true do let(:mentioner) { project.repository.commit } it 'references the mentioning commit' do - expect(subject.note).to eq "mentioned in commit #{mentioner.to_reference}" + expect(subject.note).to eq "Mentioned in commit #{mentioner.to_reference}" end end context 'from non-Commit' do it 'references the mentioning object' do - expect(subject.note).to eq "mentioned in issue #{mentioner.to_reference}" + expect(subject.note).to eq "Mentioned in issue #{mentioner.to_reference}" end end end @@ -362,7 +362,7 @@ describe SystemNoteService, services: true do describe '.cross_reference?' do it 'is truthy when text begins with expected text' do - expect(described_class.cross_reference?('mentioned in something')).to be_truthy + expect(described_class.cross_reference?('Mentioned in something')).to be_truthy end it 'is falsey when text does not begin with expected text' do diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 296fd1bd5a4..b41f6f14fbd 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -145,6 +145,14 @@ describe TodoService, services: true do end end + describe '#destroy_issue' do + it 'refresh the todos count cache for the user' do + expect(john_doe).to receive(:update_todos_count_cache).and_call_original + + service.destroy_issue(issue, john_doe) + end + end + describe '#reassigned_issue' do it 'creates a pending todo for new assignee' do unassigned_issue.update_attribute(:assignee, john_doe) @@ -394,6 +402,14 @@ describe TodoService, services: true do end end + describe '#destroy_merge_request' do + it 'refresh the todos count cache for the user' do + expect(john_doe).to receive(:update_todos_count_cache).and_call_original + + service.destroy_merge_request(mr_assigned, john_doe) + end + end + describe '#reassigned_merge_request' do it 'creates a pending todo for new assignee' do mr_unassigned.update_attribute(:assignee, john_doe) @@ -496,6 +512,7 @@ describe TodoService, services: true do describe '#mark_todos_as_done' do let(:issue) { create(:issue, project: project, author: author, assignee: john_doe) } + let(:another_issue) { create(:issue, project: project, author: author, assignee: john_doe) } it 'marks a relation of todos as done' do create(:todo, :mentioned, user: john_doe, target: issue, project: project) @@ -518,6 +535,26 @@ describe TodoService, services: true do expect(TodoService.new.mark_todos_as_done([todo], john_doe)).to eq(1) end + context 'when some of the todos are done already' do + before do + create(:todo, :mentioned, user: john_doe, target: issue, project: project) + create(:todo, :mentioned, user: john_doe, target: another_issue, project: project) + end + + it 'returns the number of those still pending' do + TodoService.new.mark_pending_todos_as_done(issue, john_doe) + + expect(TodoService.new.mark_todos_as_done(Todo.all, john_doe)).to eq(1) + end + + it 'returns 0 if all are done' do + TodoService.new.mark_pending_todos_as_done(issue, john_doe) + TodoService.new.mark_pending_todos_as_done(another_issue, john_doe) + + expect(TodoService.new.mark_todos_as_done(Todo.all, john_doe)).to eq(0) + end + end + it 'caches the number of todos of a user', :caching do create(:todo, :mentioned, user: john_doe, target: issue, project: project) todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index c144cd85487..02b2b3ca101 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -26,9 +26,9 @@ RSpec.configure do |config| config.verbose_retry = true config.display_try_failure_messages = true - config.include Devise::TestHelpers, type: :controller - config.include LoginHelpers, type: :feature - config.include LoginHelpers, type: :request + config.include Devise::TestHelpers, type: :controller + config.include Warden::Test::Helpers, type: :request + config.include LoginHelpers, type: :feature config.include StubConfiguration config.include EmailHelpers config.include TestEnv diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb index e0dbc9aa84c..ac38e31b77e 100644 --- a/spec/support/db_cleaner.rb +++ b/spec/support/db_cleaner.rb @@ -15,7 +15,7 @@ RSpec.configure do |config| DatabaseCleaner.start end - config.after(:each) do + config.append_after(:each) do DatabaseCleaner.clean end end diff --git a/spec/support/issuable_slash_commands_shared_examples.rb b/spec/support/issuable_slash_commands_shared_examples.rb index d2a49ea5c5e..5e3b8f2b23e 100644 --- a/spec/support/issuable_slash_commands_shared_examples.rb +++ b/spec/support/issuable_slash_commands_shared_examples.rb @@ -2,6 +2,9 @@ # It takes a `issuable_type`, and expect an `issuable`. shared_examples 'issuable record that supports slash commands in its description and notes' do |issuable_type| + include SlashCommandsHelpers + include WaitForAjax + let(:master) { create(:user) } let(:assignee) { create(:user, username: 'bob') } let(:guest) { create(:user) } @@ -18,6 +21,11 @@ shared_examples 'issuable record that supports slash commands in its description login_with(master) end + after do + # Ensure all outstanding Ajax requests are complete to avoid database deadlocks + wait_for_ajax + end + describe "new #{issuable_type}" do context 'with commands in the description' do it "creates the #{issuable_type} and interpret commands accordingly" do @@ -44,10 +52,7 @@ shared_examples 'issuable record that supports slash commands in its description context 'with a note containing commands' do it 'creates a note without the commands and interpret the commands accordingly' do - page.within('.js-main-target-form') do - fill_in 'note[note]', with: "Awesome!\n/assign @bob\n/label ~bug\n/milestone %\"ASAP\"" - click_button 'Comment' - end + write_note("Awesome!\n/assign @bob\n/label ~bug\n/milestone %\"ASAP\"") expect(page).to have_content 'Awesome!' expect(page).not_to have_content '/assign @bob' @@ -66,10 +71,7 @@ shared_examples 'issuable record that supports slash commands in its description context 'with a note containing only commands' do it 'does not create a note but interpret the commands accordingly' do - page.within('.js-main-target-form') do - fill_in 'note[note]', with: "/assign @bob\n/label ~bug\n/milestone %\"ASAP\"" - click_button 'Comment' - end + write_note("/assign @bob\n/label ~bug\n/milestone %\"ASAP\"") expect(page).not_to have_content '/assign @bob' expect(page).not_to have_content '/label ~bug' @@ -92,10 +94,7 @@ shared_examples 'issuable record that supports slash commands in its description context "when current user can close #{issuable_type}" do it "closes the #{issuable_type}" do - page.within('.js-main-target-form') do - fill_in 'note[note]', with: "/close" - click_button 'Comment' - end + write_note("/close") expect(page).not_to have_content '/close' expect(page).to have_content 'Your commands have been executed!' @@ -112,10 +111,7 @@ shared_examples 'issuable record that supports slash commands in its description end it "does not close the #{issuable_type}" do - page.within('.js-main-target-form') do - fill_in 'note[note]', with: "/close" - click_button 'Comment' - end + write_note("/close") expect(page).not_to have_content '/close' expect(page).not_to have_content 'Your commands have been executed!' @@ -133,10 +129,7 @@ shared_examples 'issuable record that supports slash commands in its description context "when current user can reopen #{issuable_type}" do it "reopens the #{issuable_type}" do - page.within('.js-main-target-form') do - fill_in 'note[note]', with: "/reopen" - click_button 'Comment' - end + write_note("/reopen") expect(page).not_to have_content '/reopen' expect(page).to have_content 'Your commands have been executed!' @@ -153,10 +146,7 @@ shared_examples 'issuable record that supports slash commands in its description end it "does not reopen the #{issuable_type}" do - page.within('.js-main-target-form') do - fill_in 'note[note]', with: "/reopen" - click_button 'Comment' - end + write_note("/reopen") expect(page).not_to have_content '/reopen' expect(page).not_to have_content 'Your commands have been executed!' @@ -169,10 +159,7 @@ shared_examples 'issuable record that supports slash commands in its description context "with a note changing the #{issuable_type}'s title" do context "when current user can change title of #{issuable_type}" do it "reopens the #{issuable_type}" do - page.within('.js-main-target-form') do - fill_in 'note[note]', with: "/title Awesome new title" - click_button 'Comment' - end + write_note("/title Awesome new title") expect(page).not_to have_content '/title' expect(page).to have_content 'Your commands have been executed!' @@ -189,10 +176,7 @@ shared_examples 'issuable record that supports slash commands in its description end it "does not reopen the #{issuable_type}" do - page.within('.js-main-target-form') do - fill_in 'note[note]', with: "/title Awesome new title" - click_button 'Comment' - end + write_note("/title Awesome new title") expect(page).not_to have_content '/title' expect(page).not_to have_content 'Your commands have been executed!' @@ -204,10 +188,7 @@ shared_examples 'issuable record that supports slash commands in its description context "with a note marking the #{issuable_type} as todo" do it "creates a new todo for the #{issuable_type}" do - page.within('.js-main-target-form') do - fill_in 'note[note]', with: "/todo" - click_button 'Comment' - end + write_note("/todo") expect(page).not_to have_content '/todo' expect(page).to have_content 'Your commands have been executed!' @@ -238,10 +219,7 @@ shared_examples 'issuable record that supports slash commands in its description expect(todo.author).to eq master expect(todo.user).to eq master - page.within('.js-main-target-form') do - fill_in 'note[note]', with: "/done" - click_button 'Comment' - end + write_note("/done") expect(page).not_to have_content '/done' expect(page).to have_content 'Your commands have been executed!' @@ -254,10 +232,7 @@ shared_examples 'issuable record that supports slash commands in its description it "creates a new todo for the #{issuable_type}" do expect(issuable.subscribed?(master)).to be_falsy - page.within('.js-main-target-form') do - fill_in 'note[note]', with: "/subscribe" - click_button 'Comment' - end + write_note("/subscribe") expect(page).not_to have_content '/subscribe' expect(page).to have_content 'Your commands have been executed!' @@ -274,10 +249,7 @@ shared_examples 'issuable record that supports slash commands in its description it "creates a new todo for the #{issuable_type}" do expect(issuable.subscribed?(master)).to be_truthy - page.within('.js-main-target-form') do - fill_in 'note[note]', with: "/unsubscribe" - click_button 'Comment' - end + write_note("/unsubscribe") expect(page).not_to have_content '/unsubscribe' expect(page).to have_content 'Your commands have been executed!' diff --git a/spec/support/ldap_helpers.rb b/spec/support/ldap_helpers.rb new file mode 100644 index 00000000000..079f244475c --- /dev/null +++ b/spec/support/ldap_helpers.rb @@ -0,0 +1,47 @@ +module LdapHelpers + def ldap_adapter(provider = 'ldapmain', ldap = double(:ldap)) + ::Gitlab::LDAP::Adapter.new(provider, ldap) + end + + def user_dn(uid) + "uid=#{uid},ou=users,dc=example,dc=com" + end + + # Accepts a hash of Gitlab::LDAP::Config keys and values. + # + # Example: + # stub_ldap_config( + # group_base: 'ou=groups,dc=example,dc=com', + # admin_group: 'my-admin-group' + # ) + def stub_ldap_config(messages) + messages.each do |config, value| + allow_any_instance_of(::Gitlab::LDAP::Config) + .to receive(config.to_sym).and_return(value) + end + end + + # Stub an LDAP person search and provide the return entry. Specify `nil` for + # `entry` to simulate when an LDAP person is not found + # + # Example: + # adapter = ::Gitlab::LDAP::Adapter.new('ldapmain', double(:ldap)) + # ldap_user_entry = ldap_user_entry('john_doe') + # + # stub_ldap_person_find_by_uid('john_doe', ldap_user_entry, adapter) + def stub_ldap_person_find_by_uid(uid, entry, provider = 'ldapmain') + return_value = ::Gitlab::LDAP::Person.new(entry, provider) if entry.present? + + allow(::Gitlab::LDAP::Person) + .to receive(:find_by_uid).with(uid, any_args).and_return(return_value) + end + + # Create a simple LDAP user entry. + def ldap_user_entry(uid) + entry = Net::LDAP::Entry.new + entry['dn'] = user_dn(uid) + entry['uid'] = uid + + entry + end +end diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb index e5f76afbfc0..c0b3e83244d 100644 --- a/spec/support/login_helpers.rb +++ b/spec/support/login_helpers.rb @@ -75,6 +75,7 @@ module LoginHelpers def logout find(".header-user-dropdown-toggle").click click_link "Sign out" + expect(page).to have_content('Signed out successfully') end # Logout without JavaScript driver diff --git a/spec/support/slash_commands_helpers.rb b/spec/support/slash_commands_helpers.rb new file mode 100644 index 00000000000..df483afa0e3 --- /dev/null +++ b/spec/support/slash_commands_helpers.rb @@ -0,0 +1,10 @@ +module SlashCommandsHelpers + def write_note(text) + Sidekiq::Testing.fake! do + page.within('.js-main-target-form') do + fill_in 'note[note]', with: text + click_button 'Comment' + end + end + end +end diff --git a/spec/support/taskable_shared_examples.rb b/spec/support/taskable_shared_examples.rb index 927c72c7409..201614e45a4 100644 --- a/spec/support/taskable_shared_examples.rb +++ b/spec/support/taskable_shared_examples.rb @@ -3,30 +3,57 @@ # Requires a context containing: # subject { Issue or MergeRequest } shared_examples 'a Taskable' do - before do - subject.description = <<-EOT.strip_heredoc - * [ ] Task 1 - * [x] Task 2 - * [x] Task 3 - * [ ] Task 4 - * [ ] Task 5 - EOT + describe 'with multiple tasks' do + before do + subject.description = <<-EOT.strip_heredoc + * [ ] Task 1 + * [x] Task 2 + * [x] Task 3 + * [ ] Task 4 + * [ ] Task 5 + EOT + end + + it 'returns the correct task status' do + expect(subject.task_status).to match('2 of') + expect(subject.task_status).to match('5 tasks completed') + end + + describe '#tasks?' do + it 'returns true when object has tasks' do + expect(subject.tasks?).to eq true + end + + it 'returns false when object has no tasks' do + subject.description = 'Now I have no tasks' + expect(subject.tasks?).to eq false + end + end end - it 'returns the correct task status' do - expect(subject.task_status).to match('5 tasks') - expect(subject.task_status).to match('2 completed') - expect(subject.task_status).to match('3 remaining') + describe 'with an incomplete task' do + before do + subject.description = <<-EOT.strip_heredoc + * [ ] Task 1 + EOT + end + + it 'returns the correct task status' do + expect(subject.task_status).to match('0 of') + expect(subject.task_status).to match('1 task completed') + end end - describe '#tasks?' do - it 'returns true when object has tasks' do - expect(subject.tasks?).to eq true + describe 'with a complete task' do + before do + subject.description = <<-EOT.strip_heredoc + * [x] Task 1 + EOT end - it 'returns false when object has no tasks' do - subject.description = 'Now I have no tasks' - expect(subject.tasks?).to eq false + it 'returns the correct task status' do + expect(subject.task_status).to match('1 of') + expect(subject.task_status).to match('1 task completed') end end end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index edbbfc3c9e5..0097dbf8fad 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -6,7 +6,7 @@ module TestEnv # When developing the seed repository, comment out the branch you will modify. BRANCH_SHA = { 'empty-branch' => '7efb185', - 'ends-with.json' => '98b0d8b3', + 'ends-with.json' => '98b0d8b', 'flatten-dir' => 'e56497b', 'feature' => '0b4bc9a', 'feature_conflict' => 'bb5206f', @@ -24,11 +24,12 @@ module TestEnv 'expand-collapse-lines' => '238e82d', 'video' => '8879059', 'crlf-diff' => '5938907', - 'conflict-start' => '14fa46b', + 'conflict-start' => '75284c7', 'conflict-resolvable' => '1450cd6', 'conflict-binary-file' => '259a6fb', 'conflict-contains-conflict-markers' => '5e0964c', 'conflict-missing-side' => 'eb227b3', + 'conflict-non-utf8' => 'd0a293c', 'conflict-too-large' => '39fa04f', } @@ -36,9 +37,10 @@ module TestEnv # need to keep all the branches in sync. # We currently only need a subset of the branches FORKED_BRANCH_SHA = { - 'add-submodule-version-bump' => '3f547c08', - 'master' => '5937ac0', - 'remove-submodule' => '2a33e0c0' + 'add-submodule-version-bump' => '3f547c0', + 'master' => '5937ac0', + 'remove-submodule' => '2a33e0c', + 'conflict-resolvable-fork' => '404fa3f' } # Test environment @@ -116,22 +118,7 @@ module TestEnv system(*%W(#{Gitlab.config.git.bin_path} clone -q #{clone_url} #{repo_path})) end - Dir.chdir(repo_path) do - branch_sha.each do |branch, sha| - # Try to reset without fetching to avoid using the network. - reset = %W(#{Gitlab.config.git.bin_path} update-ref refs/heads/#{branch} #{sha}) - unless system(*reset) - if system(*%W(#{Gitlab.config.git.bin_path} fetch origin)) - unless system(*reset) - raise 'The fetched test seed '\ - 'does not contain the required revision.' - end - else - raise 'Could not fetch test seed repository.' - end - end - end - end + set_repo_refs(repo_path, branch_sha) # We must copy bare repositories because we will push to them. system(git_env, *%W(#{Gitlab.config.git.bin_path} clone -q --bare #{repo_path} #{repo_path_bare})) @@ -143,6 +130,7 @@ module TestEnv FileUtils.mkdir_p(target_repo_path) FileUtils.cp_r("#{base_repo_path}/.", target_repo_path) FileUtils.chmod_R 0755, target_repo_path + set_repo_refs(target_repo_path, BRANCH_SHA) end def repos_path @@ -159,6 +147,7 @@ module TestEnv FileUtils.mkdir_p(target_repo_path) FileUtils.cp_r("#{base_repo_path}/.", target_repo_path) FileUtils.chmod_R 0755, target_repo_path + set_repo_refs(target_repo_path, FORKED_BRANCH_SHA) end # When no cached assets exist, manually hit the root path to create them @@ -208,4 +197,23 @@ module TestEnv def git_env { 'GIT_TEMPLATE_DIR' => '' } end + + def set_repo_refs(repo_path, branch_sha) + Dir.chdir(repo_path) do + branch_sha.each do |branch, sha| + # Try to reset without fetching to avoid using the network. + reset = %W(#{Gitlab.config.git.bin_path} update-ref refs/heads/#{branch} #{sha}) + unless system(*reset) + if system(*%W(#{Gitlab.config.git.bin_path} fetch origin)) + unless system(*reset) + raise 'The fetched test seed '\ + 'does not contain the required revision.' + end + else + raise 'Could not fetch test seed repository.' + end + end + end + end + end end diff --git a/spec/support/workhorse_helpers.rb b/spec/support/workhorse_helpers.rb index 107b6e30924..47673cd4c3a 100644 --- a/spec/support/workhorse_helpers.rb +++ b/spec/support/workhorse_helpers.rb @@ -13,4 +13,9 @@ module WorkhorseHelpers ] end end + + def workhorse_internal_api_request_header + jwt_token = JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') + { 'HTTP_' + Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER.upcase.tr('-', '_') => jwt_token } + end end diff --git a/spec/views/projects/builds/show.html.haml_spec.rb b/spec/views/projects/builds/show.html.haml_spec.rb index 464051063d8..446ba3bfa14 100644 --- a/spec/views/projects/builds/show.html.haml_spec.rb +++ b/spec/views/projects/builds/show.html.haml_spec.rb @@ -59,14 +59,10 @@ describe 'projects/builds/show' do end it 'shows trigger variables in separate lines' do - expect(rendered).to have_css('code', text: variable_regexp('TRIGGER_KEY_1', 'TRIGGER_VALUE_1')) - expect(rendered).to have_css('code', text: variable_regexp('TRIGGER_KEY_2', 'TRIGGER_VALUE_2')) + expect(rendered).to have_css('.js-build-variable', visible: false, text: 'TRIGGER_KEY_1') + expect(rendered).to have_css('.js-build-variable', visible: false, text: 'TRIGGER_KEY_2') + expect(rendered).to have_css('.js-build-value', visible: false, text: 'TRIGGER_VALUE_1') + expect(rendered).to have_css('.js-build-value', visible: false, text: 'TRIGGER_VALUE_2') end end - - private - - def variable_regexp(key, value) - /\A#{Regexp.escape("#{key}=#{value}")}\Z/ - end end diff --git a/spec/views/projects/merge_requests/edit.html.haml_spec.rb b/spec/views/projects/merge_requests/edit.html.haml_spec.rb new file mode 100644 index 00000000000..31bbb150698 --- /dev/null +++ b/spec/views/projects/merge_requests/edit.html.haml_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +describe 'projects/merge_requests/edit.html.haml' do + include Devise::TestHelpers + + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:fork_project) { create(:project, forked_from_project: project) } + let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } + + let(:closed_merge_request) do + create(:closed_merge_request, + source_project: fork_project, + target_project: project, + author: user) + end + + before do + assign(:project, project) + assign(:merge_request, closed_merge_request) + + allow(view).to receive(:can?).and_return(true) + allow(view).to receive(:current_user) + .and_return(User.find(closed_merge_request.author_id)) + end + + context 'when a merge request without fork' do + it "shows editable fields" do + unlink_project.execute + closed_merge_request.reload + + render + + expect(rendered).to have_field('merge_request[title]') + expect(rendered).to have_field('merge_request[description]') + expect(rendered).to have_selector('#merge_request_assignee_id', visible: false) + expect(rendered).to have_selector('#merge_request_milestone_id', visible: false) + expect(rendered).not_to have_selector('#merge_request_target_branch', visible: false) + end + end + + context 'when a merge request with an existing source project is closed' do + it "shows editable fields" do + render + + expect(rendered).to have_field('merge_request[title]') + expect(rendered).to have_field('merge_request[description]') + expect(rendered).to have_selector('#merge_request_assignee_id', visible: false) + expect(rendered).to have_selector('#merge_request_milestone_id', visible: false) + expect(rendered).to have_selector('#merge_request_target_branch', visible: false) + end + end +end diff --git a/spec/views/projects/merge_requests/show.html.haml_spec.rb b/spec/views/projects/merge_requests/show.html.haml_spec.rb new file mode 100644 index 00000000000..fe0780e72df --- /dev/null +++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe 'projects/merge_requests/show.html.haml' do + include Devise::TestHelpers + + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:fork_project) { create(:project, forked_from_project: project) } + let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } + + let(:closed_merge_request) do + create(:closed_merge_request, + source_project: fork_project, + target_project: project, + author: user) + end + + before do + assign(:project, project) + assign(:merge_request, closed_merge_request) + assign(:commits_count, 0) + + allow(view).to receive(:can?).and_return(true) + end + + context 'when the merge request is closed' do + it 'shows the "Reopen" button' do + render + + expect(rendered).to have_css('a', visible: true, text: 'Reopen') + expect(rendered).to have_css('a', visible: false, text: 'Close') + end + + it 'does not show the "Reopen" button when the source project does not exist' do + unlink_project.execute + closed_merge_request.reload + + render + + expect(rendered).to have_css('a', visible: false, text: 'Reopen') + expect(rendered).to have_css('a', visible: false, text: 'Close') + end + end +end diff --git a/spec/workers/prune_old_events_worker_spec.rb b/spec/workers/prune_old_events_worker_spec.rb new file mode 100644 index 00000000000..35e1518a35e --- /dev/null +++ b/spec/workers/prune_old_events_worker_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe PruneOldEventsWorker do + describe '#perform' do + let!(:expired_event) { create(:event, author_id: 0, created_at: 13.months.ago) } + let!(:not_expired_event) { create(:event, author_id: 0, created_at: 1.day.ago) } + let!(:exactly_12_months_event) { create(:event, author_id: 0, created_at: 12.months.ago) } + + it 'prunes events older than 12 months' do + expect { subject.perform }.to change { Event.count }.by(-1) + expect(Event.find_by(id: expired_event.id)).to be_nil + end + + it 'leaves fresh events' do + subject.perform + expect(not_expired_event.reload).to be_present + end + + it 'leaves events from exactly 12 months ago' do + subject.perform + expect(exactly_12_months_event).to be_present + end + end +end diff --git a/spec/workers/repository_check/single_repository_worker_spec.rb b/spec/workers/repository_check/single_repository_worker_spec.rb index 05e07789dac..59cfb2c8e3a 100644 --- a/spec/workers/repository_check/single_repository_worker_spec.rb +++ b/spec/workers/repository_check/single_repository_worker_spec.rb @@ -5,7 +5,7 @@ describe RepositoryCheck::SingleRepositoryWorker do subject { described_class.new } it 'passes when the project has no push events' do - project = create(:project_empty_repo, wiki_enabled: false) + project = create(:project_empty_repo, wiki_access_level: ProjectFeature::DISABLED) project.events.destroy_all break_repo(project) @@ -25,7 +25,7 @@ describe RepositoryCheck::SingleRepositoryWorker do end it 'fails if the wiki repository is broken' do - project = create(:project_empty_repo, wiki_enabled: true) + project = create(:project_empty_repo, wiki_access_level: ProjectFeature::ENABLED) project.create_wiki # Test sanity: everything should be fine before the wiki repo is broken @@ -39,7 +39,7 @@ describe RepositoryCheck::SingleRepositoryWorker do end it 'skips wikis when disabled' do - project = create(:project_empty_repo, wiki_enabled: false) + project = create(:project_empty_repo, wiki_access_level: ProjectFeature::DISABLED) # Make sure the test would fail if the wiki repo was checked break_wiki(project) @@ -49,7 +49,7 @@ describe RepositoryCheck::SingleRepositoryWorker do end it 'creates missing wikis' do - project = create(:project_empty_repo, wiki_enabled: true) + project = create(:project_empty_repo, wiki_access_level: ProjectFeature::ENABLED) FileUtils.rm_rf(wiki_path(project)) subject.perform(project.id) |