diff options
Diffstat (limited to 'spec')
506 files changed, 15453 insertions, 4289 deletions
diff --git a/spec/config/mail_room_spec.rb b/spec/config/mail_room_spec.rb index 294fae95752..0b8ff006d22 100644 --- a/spec/config/mail_room_spec.rb +++ b/spec/config/mail_room_spec.rb @@ -8,7 +8,7 @@ describe 'mail_room.yml' do context 'when incoming email is disabled' do before do - ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = Rails.root.join('spec/fixtures/mail_room_disabled.yml').to_s + ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = Rails.root.join('spec/fixtures/config/mail_room_disabled.yml').to_s Gitlab::MailRoom.reset_config! end @@ -26,7 +26,7 @@ describe 'mail_room.yml' do let(:gitlab_redis) { Gitlab::Redis.new(Rails.env) } before do - ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = Rails.root.join('spec/fixtures/mail_room_enabled.yml').to_s + ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = Rails.root.join('spec/fixtures/config/mail_room_enabled.yml').to_s Gitlab::MailRoom.reset_config! end diff --git a/spec/controllers/admin/runners_controller_spec.rb b/spec/controllers/admin/runners_controller_spec.rb new file mode 100644 index 00000000000..b5fe40d0510 --- /dev/null +++ b/spec/controllers/admin/runners_controller_spec.rb @@ -0,0 +1,85 @@ +require 'spec_helper' + +describe Admin::RunnersController do + let(:runner) { create(:ci_runner) } + + before do + sign_in(create(:admin)) + end + + describe '#index' do + it 'lists all runners' do + get :index + + expect(response).to have_http_status(200) + end + end + + describe '#show' do + it 'shows a particular runner' do + get :show, id: runner.id + + expect(response).to have_http_status(200) + end + + it 'shows 404 for unknown runner' do + get :show, id: 0 + + expect(response).to have_http_status(404) + end + end + + describe '#update' do + it 'updates the runner and ticks the queue' do + new_desc = runner.description.swapcase + + expect do + post :update, id: runner.id, runner: { description: new_desc } + end.to change { runner.ensure_runner_queue_value } + + runner.reload + + expect(response).to have_http_status(302) + expect(runner.description).to eq(new_desc) + end + end + + describe '#destroy' do + it 'destroys the runner' do + delete :destroy, id: runner.id + + expect(response).to have_http_status(302) + expect(Ci::Runner.find_by(id: runner.id)).to be_nil + end + end + + describe '#resume' do + it 'marks the runner as active and ticks the queue' do + runner.update(active: false) + + expect do + post :resume, id: runner.id + end.to change { runner.ensure_runner_queue_value } + + runner.reload + + expect(response).to have_http_status(302) + expect(runner.active).to eq(true) + end + end + + describe '#pause' do + it 'marks the runner as inactive and ticks the queue' do + runner.update(active: true) + + expect do + post :pause, id: runner.id + end.to change { runner.ensure_runner_queue_value } + + runner.reload + + expect(response).to have_http_status(302) + expect(runner.active).to eq(false) + end + end +end diff --git a/spec/controllers/blob_controller_spec.rb b/spec/controllers/blob_controller_spec.rb index 2fcb4a6a528..44e011fd3a8 100644 --- a/spec/controllers/blob_controller_spec.rb +++ b/spec/controllers/blob_controller_spec.rb @@ -19,8 +19,8 @@ describe Projects::BlobController do before do get(:show, - namespace_id: project.namespace.to_param, - project_id: project.to_param, + namespace_id: project.namespace, + project_id: project, id: id) end @@ -50,8 +50,8 @@ describe Projects::BlobController do before do get(:show, - namespace_id: project.namespace.to_param, - project_id: project.to_param, + namespace_id: project.namespace, + project_id: project, id: id) controller.instance_variable_set(:@blob, nil) end diff --git a/spec/controllers/ci/projects_controller_spec.rb b/spec/controllers/ci/projects_controller_spec.rb deleted file mode 100644 index 86f01f437a2..00000000000 --- a/spec/controllers/ci/projects_controller_spec.rb +++ /dev/null @@ -1,74 +0,0 @@ -require 'spec_helper' - -describe Ci::ProjectsController do - let(:visibility) { :public } - let!(:project) { create(:empty_project, visibility, ci_id: 1) } - let(:ci_id) { project.ci_id } - - describe '#index' do - context 'user signed in' do - before do - sign_in(create(:user)) - get(:index) - end - - it 'redirects to /' do - expect(response).to redirect_to(root_path) - end - end - - context 'user not signed in' do - before { get(:index) } - - it 'redirects to sign in page' do - expect(response).to redirect_to(new_user_session_path) - end - end - end - - ## - # Specs for *deprecated* CI badge - # - describe '#badge' do - shared_examples 'badge provider' do - it 'shows badge' do - expect(response.status).to eq 200 - expect(response.headers) - .to include('Content-Type' => 'image/svg+xml') - end - end - - context 'user not signed in' do - before { get(:badge, id: ci_id) } - - context 'project has no ci_id reference' do - let(:ci_id) { 123 } - - it 'returns 404' do - expect(response.status).to eq 404 - end - end - - context 'project is public' do - let(:visibility) { :public } - it_behaves_like 'badge provider' - end - - context 'project is private' do - let(:visibility) { :private } - it_behaves_like 'badge provider' - end - end - - context 'user signed in' do - let(:user) { create(:user) } - before { sign_in(user) } - before { get(:badge, id: ci_id) } - - context 'private is internal' do - let(:visibility) { :internal } - it_behaves_like 'badge provider' - end - end - end -end diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb index 79ef3a1adad..7072bd5e87c 100644 --- a/spec/controllers/dashboard/todos_controller_spec.rb +++ b/spec/controllers/dashboard/todos_controller_spec.rb @@ -1,16 +1,19 @@ require 'spec_helper' describe Dashboard::TodosController do + include ApiHelpers + let(:user) { create(:user) } + let(:author) { create(:user) } let(:project) { create(:empty_project) } let(:todo_service) { TodoService.new } - describe 'GET #index' do - before do - sign_in(user) - project.team << [user, :developer] - end + before do + sign_in(user) + project.team << [user, :developer] + end + describe 'GET #index' do context 'when using pagination' do let(:last_page) { user.todos.page.total_pages } let!(:issues) { create_list(:issue, 2, project: project, assignee: user) } @@ -34,4 +37,16 @@ describe Dashboard::TodosController do end end end + + describe 'PATCH #restore' do + let(:todo) { create(:todo, :done, user: user, project: project, author: author) } + + it 'restores the todo to pending state' do + patch :restore, id: todo.id + + expect(todo.reload).to be_pending + expect(response).to have_http_status(200) + expect(json_response).to eq({ "count" => "1", "done_count" => "0" }) + end + end end diff --git a/spec/controllers/dashboard_controller_spec.rb b/spec/controllers/dashboard_controller_spec.rb new file mode 100644 index 00000000000..566d8515198 --- /dev/null +++ b/spec/controllers/dashboard_controller_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe DashboardController do + let(:user) { create(:user) } + let(:project) { create(:project) } + + before do + project.team << [user, :master] + sign_in(user) + end + + describe 'GET issues' do + it_behaves_like 'issuables list meta-data', :issue, :issues + end + + describe 'GET merge requests' do + it_behaves_like 'issuables list meta-data', :merge_request, :merge_requests + end +end diff --git a/spec/controllers/health_check_controller_spec.rb b/spec/controllers/health_check_controller_spec.rb index cfe18dd4b6c..58c16cc57e6 100644 --- a/spec/controllers/health_check_controller_spec.rb +++ b/spec/controllers/health_check_controller_spec.rb @@ -64,8 +64,8 @@ describe HealthCheckController do context 'when a service is down and an access token is provided' do before do - allow(HealthCheck::Utils).to receive(:process_checks).with('standard').and_return('The server is on fire') - allow(HealthCheck::Utils).to receive(:process_checks).with('email').and_return('Email is on fire') + allow(HealthCheck::Utils).to receive(:process_checks).with(['standard']).and_return('The server is on fire') + allow(HealthCheck::Utils).to receive(:process_checks).with(['email']).and_return('Email is on fire') end it 'supports passing the token in the header' do diff --git a/spec/controllers/profiles/keys_controller_spec.rb b/spec/controllers/profiles/keys_controller_spec.rb index 6bcfae0fc13..f7219690722 100644 --- a/spec/controllers/profiles/keys_controller_spec.rb +++ b/spec/controllers/profiles/keys_controller_spec.rb @@ -42,10 +42,9 @@ describe Profiles::KeysController do end describe "user with keys" do - before do - user.keys << create(:key) - user.keys << create(:another_key) - end + let!(:key) { create(:key, user: user) } + let!(:another_key) { create(:another_key, user: user) } + let!(:deploy_key) { create(:deploy_key, user: user) } it "does generally work" do get :get_keys, username: user.username @@ -53,16 +52,16 @@ describe Profiles::KeysController do expect(response).to be_success end - it "renders all keys separated with a new line" do + it "renders all non deploy keys separated with a new line" do get :get_keys, username: user.username - expect(response.body).not_to eq("") + expect(response.body).not_to eq('') expect(response.body).to eq(user.all_ssh_keys.join("\n")) - # Unique part of key 1 - expect(response.body).to match(/PWx6WM4lhHNedGfBpPJNPpZ/) - # Key 2 - expect(response.body).to match(/AQDmTillFzNTrrGgwaCKaSj/) + expect(response.body).to include(key.key.sub(' dummy@gitlab.com', '')) + expect(response.body).to include(another_key.key) + + expect(response.body).not_to include(deploy_key.key) end it "does not render the comment of the key" do diff --git a/spec/controllers/profiles/notifications_controller_spec.rb b/spec/controllers/profiles/notifications_controller_spec.rb new file mode 100644 index 00000000000..58caf7999cf --- /dev/null +++ b/spec/controllers/profiles/notifications_controller_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +describe Profiles::NotificationsController do + let(:user) do + create(:user) do |user| + user.emails.create(email: 'original@example.com') + user.emails.create(email: 'new@example.com') + user.update(notification_email: 'original@example.com') + user.save! + end + end + + describe 'GET show' do + it 'renders' do + sign_in(user) + + get :show + + expect(response).to render_template :show + end + end + + describe 'POST update' do + it 'updates only permitted attributes' do + sign_in(user) + + put :update, user: { notification_email: 'new@example.com', notified_of_own_activity: true, admin: true } + + user.reload + expect(user.notification_email).to eq('new@example.com') + expect(user.notified_of_own_activity).to eq(true) + expect(user.admin).to eq(false) + expect(controller).to set_flash[:notice].to('Notification settings saved') + end + + it 'shows an error message if the params are invalid' do + sign_in(user) + + put :update, user: { notification_email: '' } + + expect(user.reload.notification_email).to eq('original@example.com') + expect(controller).to set_flash[:alert].to('Failed to save new settings') + end + end +end diff --git a/spec/controllers/profiles/personal_access_tokens_spec.rb b/spec/controllers/profiles/personal_access_tokens_spec.rb index 45534a3a587..9d5f4c99f6d 100644 --- a/spec/controllers/profiles/personal_access_tokens_spec.rb +++ b/spec/controllers/profiles/personal_access_tokens_spec.rb @@ -32,10 +32,10 @@ describe Profiles::PersonalAccessTokensController do context "scopes" do it "allows creation of a token with scopes" do - post :create, personal_access_token: { name: FFaker::Product.brand, scopes: ['api', 'read_user'] } + post :create, personal_access_token: { name: FFaker::Product.brand, scopes: %w(api read_user) } expect(created_token).not_to be_nil - expect(created_token.scopes).to eq(['api', 'read_user']) + expect(created_token.scopes).to eq(%w(api read_user)) end it "allows creation of a token with no scopes" do diff --git a/spec/controllers/profiles/preferences_controller_spec.rb b/spec/controllers/profiles/preferences_controller_spec.rb index 8f02003992a..7b3aa0491c7 100644 --- a/spec/controllers/profiles/preferences_controller_spec.rb +++ b/spec/controllers/profiles/preferences_controller_spec.rb @@ -25,8 +25,7 @@ describe Profiles::PreferencesController do def go(params: {}, format: :js) params.reverse_merge!( color_scheme_id: '1', - dashboard: 'stars', - theme_id: '1' + dashboard: 'stars' ) patch :update, user: params, format: format @@ -41,8 +40,7 @@ describe Profiles::PreferencesController do it "changes the user's preferences" do prefs = { color_scheme_id: '1', - dashboard: 'stars', - theme_id: '2' + dashboard: 'stars' }.with_indifferent_access expect(user).to receive(:update_attributes).with(prefs) diff --git a/spec/controllers/projects/blame_controller_spec.rb b/spec/controllers/projects/blame_controller_spec.rb index addc5e7ec33..c086b386381 100644 --- a/spec/controllers/projects/blame_controller_spec.rb +++ b/spec/controllers/projects/blame_controller_spec.rb @@ -16,8 +16,8 @@ describe Projects::BlameController do before do get(:show, - namespace_id: project.namespace.to_param, - project_id: project.to_param, + namespace_id: project.namespace, + project_id: project, id: id) end diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb index b36d0e69330..ec36a64b415 100644 --- a/spec/controllers/projects/blob_controller_spec.rb +++ b/spec/controllers/projects/blob_controller_spec.rb @@ -14,8 +14,8 @@ describe Projects::BlobController do render_views def do_get(opts = {}) - params = { namespace_id: project.namespace.to_param, - project_id: project.to_param, + params = { namespace_id: project.namespace, + project_id: project, id: 'master/CHANGELOG' } get :diff, params.merge(opts) end @@ -40,8 +40,8 @@ describe Projects::BlobController do describe 'PUT update' do let(:default_params) do { - namespace_id: project.namespace.to_param, - project_id: project.to_param, + namespace_id: project.namespace, + project_id: project, id: 'master/CHANGELOG', target_branch: 'master', content: 'Added changes', @@ -86,32 +86,47 @@ describe Projects::BlobController do end context 'when user has forked project' do - let(:guest) { create(:user) } - let!(:forked_project) { Projects::ForkService.new(project, guest).execute } - let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, source_branch: "fork-test-1", target_branch: "master") } - - before { sign_in(guest) } - - it "redirects to forked project new merge request" do - default_params[:target_branch] = "fork-test-1" - default_params[:create_merge_request] = 1 - - allow_any_instance_of(Files::UpdateService).to receive(:commit).and_return(:success) - - put :update, default_params - - expect(response).to redirect_to( - new_namespace_project_merge_request_path( - forked_project.namespace, - forked_project, - merge_request: { - source_project_id: forked_project.id, - target_project_id: project.id, - source_branch: "fork-test-1", - target_branch: "master" - } + let(:forked_project_link) { create(:forked_project_link, forked_from_project: project) } + let!(:forked_project) { forked_project_link.forked_to_project } + let(:guest) { forked_project.owner } + + before do + sign_in(guest) + end + + context 'when editing on the fork' do + before do + default_params[:namespace_id] = forked_project.namespace + default_params[:project_id] = forked_project + end + + it 'redirects to blob' do + put :update, default_params + + expect(response).to redirect_to(namespace_project_blob_path(forked_project.namespace, forked_project, 'master/CHANGELOG')) + end + end + + context 'when editing on the original repository' do + it "redirects to forked project new merge request" do + default_params[:target_branch] = "fork-test-1" + default_params[:create_merge_request] = 1 + + put :update, default_params + + expect(response).to redirect_to( + new_namespace_project_merge_request_path( + forked_project.namespace, + forked_project, + merge_request: { + source_project_id: forked_project.id, + target_project_id: project.id, + source_branch: "fork-test-1", + target_branch: "master" + } + ) ) - ) + end end end end diff --git a/spec/controllers/projects/boards/issues_controller_spec.rb b/spec/controllers/projects/boards/issues_controller_spec.rb index ad15e3942a5..3d0533cb516 100644 --- a/spec/controllers/projects/boards/issues_controller_spec.rb +++ b/spec/controllers/projects/boards/issues_controller_spec.rb @@ -90,7 +90,7 @@ describe Projects::Boards::IssuesController do params = { namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, board_id: board.to_param, list_id: list.try(:to_param) } @@ -146,7 +146,7 @@ describe Projects::Boards::IssuesController do sign_in(user) post :create, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, board_id: board.to_param, list_id: list.to_param, issue: { title: title }, @@ -209,7 +209,7 @@ describe Projects::Boards::IssuesController do sign_in(user) patch :update, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, board_id: board.to_param, id: issue.to_param, from_list_id: from_list_id, diff --git a/spec/controllers/projects/boards/lists_controller_spec.rb b/spec/controllers/projects/boards/lists_controller_spec.rb index b3f9f76a50c..432f3c53c90 100644 --- a/spec/controllers/projects/boards/lists_controller_spec.rb +++ b/spec/controllers/projects/boards/lists_controller_spec.rb @@ -47,7 +47,7 @@ describe Projects::Boards::ListsController do sign_in(user) get :index, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, board_id: board.to_param, format: :json end @@ -104,7 +104,7 @@ describe Projects::Boards::ListsController do sign_in(user) post :create, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, board_id: board.to_param, list: { label_id: label_id }, format: :json @@ -157,7 +157,7 @@ describe Projects::Boards::ListsController do sign_in(user) patch :update, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, board_id: board.to_param, id: list.to_param, list: { position: position }, @@ -200,7 +200,7 @@ describe Projects::Boards::ListsController do sign_in(user) delete :destroy, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, board_id: board.to_param, id: list.to_param, format: :json @@ -244,7 +244,7 @@ describe Projects::Boards::ListsController do sign_in(user) post :generate, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, board_id: board.to_param, format: :json end diff --git a/spec/controllers/projects/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb index cc19035740e..aed3a45c413 100644 --- a/spec/controllers/projects/boards_controller_spec.rb +++ b/spec/controllers/projects/boards_controller_spec.rb @@ -50,8 +50,8 @@ describe Projects::BoardsController do end def list_boards(format: :html) - get :index, namespace_id: project.namespace.to_param, - project_id: project.to_param, + get :index, namespace_id: project.namespace, + project_id: project, format: format end end @@ -100,8 +100,8 @@ describe Projects::BoardsController do end def read_board(board:, format: :html) - get :show, namespace_id: project.namespace.to_param, - project_id: project.to_param, + get :show, namespace_id: project.namespace, + project_id: project, id: board.to_param, format: format end diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb index 9de03876755..e70737376af 100644 --- a/spec/controllers/projects/branches_controller_spec.rb +++ b/spec/controllers/projects/branches_controller_spec.rb @@ -22,8 +22,8 @@ describe Projects::BranchesController do sign_in(user) post :create, - namespace_id: project.namespace.to_param, - project_id: project.to_param, + namespace_id: project.namespace, + project_id: project, branch_name: branch, ref: ref end @@ -68,7 +68,7 @@ describe Projects::BranchesController do describe "created from the new branch button on issues" do let(:branch) { "1-feature-branch" } - let!(:issue) { create(:issue, project: project) } + let(:issue) { create(:issue, project: project) } before do sign_in(user) @@ -76,8 +76,8 @@ describe Projects::BranchesController do it 'redirects' do post :create, - namespace_id: project.namespace.to_param, - project_id: project.to_param, + namespace_id: project.namespace, + project_id: project, branch_name: branch, issue_iid: issue.iid @@ -89,12 +89,49 @@ describe Projects::BranchesController do expect(SystemNoteService).to receive(:new_issue_branch).with(issue, project, user, "1-feature-branch") post :create, - namespace_id: project.namespace.to_param, - project_id: project.to_param, + namespace_id: project.namespace, + project_id: project, branch_name: branch, issue_iid: issue.iid end + context 'repository-less project' do + let(:project) { create :empty_project } + + it 'redirects to newly created branch' do + result = { status: :success, branch: double(name: branch) } + + expect_any_instance_of(CreateBranchService).to receive(:execute).and_return(result) + expect(SystemNoteService).to receive(:new_issue_branch).and_return(true) + + post :create, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + branch_name: branch, + issue_iid: issue.iid + + expect(response).to redirect_to namespace_project_tree_path(project.namespace, project, branch) + end + + it 'redirects to autodeploy setup page' do + result = { status: :success, branch: double(name: branch) } + + project.services << build(:kubernetes_service) + + expect_any_instance_of(CreateBranchService).to receive(:execute).and_return(result) + expect(SystemNoteService).to receive(:new_issue_branch).and_return(true) + + post :create, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + branch_name: branch, + issue_iid: issue.iid + + expect(response.location).to include(namespace_project_new_blob_path(project.namespace, project, branch)) + expect(response).to have_http_status(302) + end + end + context 'without issue feature access' do before do project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) @@ -106,8 +143,8 @@ describe Projects::BranchesController do expect(SystemNoteService).not_to receive(:new_issue_branch) post :create, - namespace_id: project.namespace.to_param, - project_id: project.to_param, + namespace_id: project.namespace, + project_id: project, branch_name: branch, issue_iid: issue.iid end @@ -126,8 +163,8 @@ describe Projects::BranchesController do post :destroy, format: :html, id: 'foo/bar/baz', - namespace_id: project.namespace.to_param, - project_id: project.to_param + namespace_id: project.namespace, + project_id: project expect(response).to have_http_status(303) end @@ -142,8 +179,8 @@ describe Projects::BranchesController do post :destroy, format: :js, id: branch, - namespace_id: project.namespace.to_param, - project_id: project.to_param + namespace_id: project.namespace, + project_id: project end context "valid branch name, valid source" do @@ -173,8 +210,8 @@ describe Projects::BranchesController do describe "DELETE destroy_all_merged" do def destroy_all_merged delete :destroy_all_merged, - namespace_id: project.namespace.to_param, - project_id: project.to_param + namespace_id: project.namespace, + project_id: project end context 'when user is allowed to push' do diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb index ebd2d0e092b..b223a22ae60 100644 --- a/spec/controllers/projects/commit_controller_spec.rb +++ b/spec/controllers/projects/commit_controller_spec.rb @@ -17,8 +17,8 @@ describe Projects::CommitController do def go(extra_params = {}) params = { - namespace_id: project.namespace.to_param, - project_id: project.to_param + namespace_id: project.namespace, + project_id: project } get :show, params.merge(extra_params) @@ -125,8 +125,8 @@ describe Projects::CommitController do it 'renders it' do get(:show, - namespace_id: fork_project.namespace.to_param, - project_id: fork_project.to_param, + namespace_id: fork_project.namespace, + project_id: fork_project, id: commit.id) expect(response).to be_success @@ -139,8 +139,8 @@ describe Projects::CommitController do commit = project.commit('5937ac0a7beb003549fc5fd26fc247adbce4a52e') get(:branches, - namespace_id: project.namespace.to_param, - project_id: project.to_param, + namespace_id: project.namespace, + project_id: project, id: commit.id) expect(assigns(:branches)).to include("master", "feature_conflict") @@ -152,8 +152,8 @@ describe Projects::CommitController do context 'when target branch is not provided' do it 'renders the 404 page' do post(:revert, - namespace_id: project.namespace.to_param, - project_id: project.to_param, + namespace_id: project.namespace, + project_id: project, id: commit.id) expect(response).not_to be_success @@ -164,9 +164,9 @@ describe Projects::CommitController do context 'when the revert was successful' do it 'redirects to the commits page' do post(:revert, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - target_branch: 'master', + namespace_id: project.namespace, + project_id: project, + start_branch: 'master', id: commit.id) expect(response).to redirect_to namespace_project_commits_path(project.namespace, project, 'master') @@ -177,18 +177,18 @@ describe Projects::CommitController do context 'when the revert failed' do before do post(:revert, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - target_branch: 'master', + namespace_id: project.namespace, + project_id: project, + start_branch: 'master', id: commit.id) end it 'redirects to the commit page' do # Reverting a commit that has been already reverted. post(:revert, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - target_branch: 'master', + namespace_id: project.namespace, + project_id: project, + start_branch: 'master', id: commit.id) expect(response).to redirect_to namespace_project_commit_path(project.namespace, project, commit.id) @@ -201,8 +201,8 @@ describe Projects::CommitController do context 'when target branch is not provided' do it 'renders the 404 page' do post(:cherry_pick, - namespace_id: project.namespace.to_param, - project_id: project.to_param, + namespace_id: project.namespace, + project_id: project, id: master_pickable_commit.id) expect(response).not_to be_success @@ -213,9 +213,9 @@ describe Projects::CommitController do context 'when the cherry-pick was successful' do it 'redirects to the commits page' do post(:cherry_pick, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - target_branch: 'master', + namespace_id: project.namespace, + project_id: project, + start_branch: 'master', id: master_pickable_commit.id) expect(response).to redirect_to namespace_project_commits_path(project.namespace, project, 'master') @@ -226,18 +226,18 @@ describe Projects::CommitController do context 'when the cherry_pick failed' do before do post(:cherry_pick, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - target_branch: 'master', + namespace_id: project.namespace, + project_id: project, + start_branch: 'master', id: master_pickable_commit.id) end it 'redirects to the commit page' do # Cherry-picking a commit that has been already cherry-picked. post(:cherry_pick, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - target_branch: 'master', + namespace_id: project.namespace, + project_id: project, + start_branch: 'master', id: master_pickable_commit.id) expect(response).to redirect_to namespace_project_commit_path(project.namespace, project, master_pickable_commit.id) @@ -249,8 +249,8 @@ describe Projects::CommitController do describe 'GET diff_for_path' do def diff_for_path(extra_params = {}) params = { - namespace_id: project.namespace.to_param, - project_id: project.to_param + namespace_id: project.namespace, + project_id: project } get :diff_for_path, params.merge(extra_params) @@ -313,8 +313,8 @@ describe Projects::CommitController do describe 'GET pipelines' do def get_pipelines(extra_params = {}) params = { - namespace_id: project.namespace.to_param, - project_id: project.to_param + namespace_id: project.namespace, + project_id: project } get :pipelines, params.merge(extra_params) diff --git a/spec/controllers/projects/commits_controller_spec.rb b/spec/controllers/projects/commits_controller_spec.rb index 54b8d1108a5..e26731fb691 100644 --- a/spec/controllers/projects/commits_controller_spec.rb +++ b/spec/controllers/projects/commits_controller_spec.rb @@ -16,8 +16,8 @@ describe Projects::CommitsController do context "when the ref does not exist with the suffix" do it "renders as atom" do get(:show, - namespace_id: project.namespace.to_param, - project_id: project.to_param, + namespace_id: project.namespace, + project_id: project, id: "master.atom") expect(response).to be_success @@ -33,8 +33,8 @@ describe Projects::CommitsController do allow_any_instance_of(Repository).to receive(:commit).with('master.atom').and_return(commit) get(:show, - namespace_id: project.namespace.to_param, - project_id: project.to_param, + namespace_id: project.namespace, + project_id: project, id: "master.atom") end diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb index e811c76fb31..15ac4e0925a 100644 --- a/spec/controllers/projects/compare_controller_spec.rb +++ b/spec/controllers/projects/compare_controller_spec.rb @@ -13,8 +13,8 @@ describe Projects::CompareController do it 'compare shows some diffs' do get(:show, - namespace_id: project.namespace.to_param, - project_id: project.to_param, + namespace_id: project.namespace, + project_id: project, from: ref_from, to: ref_to) @@ -25,8 +25,8 @@ describe Projects::CompareController do it 'compare shows some diffs with ignore whitespace change option' do get(:show, - namespace_id: project.namespace.to_param, - project_id: project.to_param, + namespace_id: project.namespace, + project_id: project, from: '08f22f25', to: '66eceea0', w: 1) @@ -43,8 +43,8 @@ describe Projects::CompareController do describe 'non-existent refs' do it 'uses invalid source ref' do get(:show, - namespace_id: project.namespace.to_param, - project_id: project.to_param, + namespace_id: project.namespace, + project_id: project, from: 'non-existent', to: ref_to) @@ -55,8 +55,8 @@ describe Projects::CompareController do it 'uses invalid target ref' do get(:show, - namespace_id: project.namespace.to_param, - project_id: project.to_param, + namespace_id: project.namespace, + project_id: project, from: ref_from, to: 'non-existent') @@ -67,8 +67,8 @@ describe Projects::CompareController do it 'redirects back to index when params[:from] is empty and preserves params[:to]' do post(:create, - namespace_id: project.namespace.to_param, - project_id: project.to_param, + namespace_id: project.namespace, + project_id: project, from: '', to: 'master') @@ -77,8 +77,8 @@ describe Projects::CompareController do it 'redirects back to index when params[:to] is empty and preserves params[:from]' do post(:create, - namespace_id: project.namespace.to_param, - project_id: project.to_param, + namespace_id: project.namespace, + project_id: project, from: 'master', to: '') @@ -87,8 +87,8 @@ describe Projects::CompareController do it 'redirects back to index when params[:from] and params[:to] are empty' do post(:create, - namespace_id: project.namespace.to_param, - project_id: project.to_param, + namespace_id: project.namespace, + project_id: project, from: '', to: '') @@ -99,8 +99,8 @@ describe Projects::CompareController do describe 'GET diff_for_path' do def diff_for_path(extra_params = {}) params = { - namespace_id: project.namespace.to_param, - project_id: project.to_param + namespace_id: project.namespace, + project_id: project } get :diff_for_path, params.merge(extra_params) diff --git a/spec/controllers/projects/cycle_analytics_controller_spec.rb b/spec/controllers/projects/cycle_analytics_controller_spec.rb index 6a6d71a16ee..6fae52edbad 100644 --- a/spec/controllers/projects/cycle_analytics_controller_spec.rb +++ b/spec/controllers/projects/cycle_analytics_controller_spec.rb @@ -13,8 +13,8 @@ describe Projects::CycleAnalyticsController do context 'with no data' do it 'is true' do get(:show, - namespace_id: project.namespace.to_param, - project_id: project.to_param) + namespace_id: project.namespace, + project_id: project) expect(response).to be_success expect(assigns(:cycle_analytics_no_data)).to eq(true) @@ -32,8 +32,8 @@ describe Projects::CycleAnalyticsController do it 'is false' do get(:show, - namespace_id: project.namespace.to_param, - project_id: project.to_param) + namespace_id: project.namespace, + project_id: project) expect(response).to be_success expect(assigns(:cycle_analytics_no_data)).to eq(false) diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb index 7ac1d62d1b1..84d119f1867 100644 --- a/spec/controllers/projects/environments_controller_spec.rb +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -3,9 +3,12 @@ require 'spec_helper' describe Projects::EnvironmentsController do include ApiHelpers - let(:environment) { create(:environment) } - let(:project) { environment.project } - let(:user) { create(:user) } + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + + let(:environment) do + create(:environment, name: 'production', project: project) + end before do project.team << [user, :master] @@ -22,14 +25,58 @@ describe Projects::EnvironmentsController do end end - context 'when requesting JSON response' do - it 'responds with correct JSON' do - get :index, environment_params(format: :json) + context 'when requesting JSON response for folders' do + before do + create(:environment, project: project, + name: 'staging/review-1', + state: :available) + + create(:environment, project: project, + name: 'staging/review-2', + state: :available) + + create(:environment, project: project, + name: 'staging/review-3', + state: :stopped) + end + + let(:environments) { json_response['environments'] } + + context 'when requesting available environments scope' do + before do + get :index, environment_params(format: :json, scope: :available) + end + + it 'responds with a payload describing available environments' do + expect(environments.count).to eq 2 + expect(environments.first['name']).to eq 'production' + expect(environments.second['name']).to eq 'staging' + expect(environments.second['size']).to eq 2 + expect(environments.second['latest']['name']).to eq 'staging/review-2' + end - first_environment = json_response.first + it 'contains values describing environment scopes sizes' do + expect(json_response['available_count']).to eq 3 + expect(json_response['stopped_count']).to eq 1 + end + end - expect(first_environment).not_to be_empty - expect(first_environment['name']). to eq environment.name + context 'when requesting stopped environments scope' do + before do + get :index, environment_params(format: :json, scope: :stopped) + end + + it 'responds with a payload describing stopped environments' do + expect(environments.count).to eq 1 + expect(environments.first['name']).to eq 'staging' + expect(environments.first['size']).to eq 1 + expect(environments.first['latest']['name']).to eq 'staging/review-3' + end + + it 'contains values describing environment scopes sizes' do + expect(json_response['available_count']).to eq 3 + expect(json_response['stopped_count']).to eq 1 + end end end end diff --git a/spec/controllers/projects/find_file_controller_spec.rb b/spec/controllers/projects/find_file_controller_spec.rb index a4884256c92..6a5433bcc9c 100644 --- a/spec/controllers/projects/find_file_controller_spec.rb +++ b/spec/controllers/projects/find_file_controller_spec.rb @@ -17,8 +17,8 @@ describe Projects::FindFileController do before do get(:show, - namespace_id: project.namespace.to_param, - project_id: project.to_param, + namespace_id: project.namespace, + project_id: project, id: id) end @@ -36,8 +36,8 @@ describe Projects::FindFileController do describe "GET #list" do def go(format: 'json') get :list, - namespace_id: project.namespace.to_param, - project_id: project.to_param, + namespace_id: project.namespace, + project_id: project, id: id, format: format end diff --git a/spec/controllers/projects/forks_controller_spec.rb b/spec/controllers/projects/forks_controller_spec.rb index a867668d97b..8282d79298f 100644 --- a/spec/controllers/projects/forks_controller_spec.rb +++ b/spec/controllers/projects/forks_controller_spec.rb @@ -9,8 +9,8 @@ describe Projects::ForksController do describe 'GET index' do def get_forks get :index, - namespace_id: project.namespace.to_param, - project_id: project.to_param + namespace_id: project.namespace, + project_id: project end context 'when fork is public' do @@ -71,8 +71,8 @@ describe Projects::ForksController do describe 'GET new' do def get_new get :new, - namespace_id: project.namespace.to_param, - project_id: project.to_param + namespace_id: project.namespace, + project_id: project end context 'when user is signed in' do @@ -99,8 +99,8 @@ describe Projects::ForksController do describe 'POST create' do def post_create post :create, - namespace_id: project.namespace.to_param, - project_id: project.to_param, + namespace_id: project.namespace, + project_id: project, namespace_key: user.namespace.id end diff --git a/spec/controllers/projects/graphs_controller_spec.rb b/spec/controllers/projects/graphs_controller_spec.rb index bbe8e4bf6b2..049bae1899d 100644 --- a/spec/controllers/projects/graphs_controller_spec.rb +++ b/spec/controllers/projects/graphs_controller_spec.rb @@ -9,7 +9,23 @@ describe Projects::GraphsController do project.team << [user, :master] end - describe 'GET #languages' do + describe 'GET languages' do + it "redirects_to action charts" do + get(:commits, namespace_id: project.namespace.path, project_id: project.path, id: 'master') + + expect(response).to redirect_to action: :charts + end + end + + describe 'GET commits' do + it "redirects_to action charts" do + get(:commits, namespace_id: project.namespace.path, project_id: project.path, id: 'master') + + expect(response).to redirect_to action: :charts + end + end + + describe 'GET charts' do let(:linguist_repository) do double(languages: { 'Ruby' => 1000, @@ -34,7 +50,7 @@ describe Projects::GraphsController do end it 'sets the correct colour according to language' do - get(:languages, namespace_id: project.namespace.path, project_id: project.path, id: 'master') + get(:charts, namespace_id: project.namespace, project_id: project, id: 'master') expected_values.each do |val| expect(assigns(:languages)).to include(a_hash_including(val)) diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb index a976a9c27ab..ca4a8e871c0 100644 --- a/spec/controllers/projects/group_links_controller_spec.rb +++ b/spec/controllers/projects/group_links_controller_spec.rb @@ -14,8 +14,8 @@ describe Projects::GroupLinksController do describe '#create' do shared_context 'link project to group' do before do - post(:create, namespace_id: project.namespace.to_param, - project_id: project.to_param, + post(:create, namespace_id: project.namespace, + project_id: project, link_group_id: group.id, link_group_access: ProjectGroupLink.default_access) end @@ -50,8 +50,8 @@ describe Projects::GroupLinksController do context 'when project group id equal link group id' do before do - post(:create, namespace_id: project.namespace.to_param, - project_id: project.to_param, + post(:create, namespace_id: project.namespace, + project_id: project, link_group_id: group2.id, link_group_access: ProjectGroupLink.default_access) end @@ -69,8 +69,8 @@ describe Projects::GroupLinksController do context 'when link group id is not present' do before do - post(:create, namespace_id: project.namespace.to_param, - project_id: project.to_param, + post(:create, namespace_id: project.namespace, + project_id: project, link_group_access: ProjectGroupLink.default_access) end diff --git a/spec/controllers/projects/imports_controller_spec.rb b/spec/controllers/projects/imports_controller_spec.rb index 2acbba469e3..7c75815f3c4 100644 --- a/spec/controllers/projects/imports_controller_spec.rb +++ b/spec/controllers/projects/imports_controller_spec.rb @@ -13,13 +13,13 @@ describe Projects::ImportsController do end it 'renders template' do - get :show, namespace_id: project.namespace.to_param, project_id: project.to_param + get :show, namespace_id: project.namespace.to_param, project_id: project expect(response).to render_template :show end it 'sets flash.now if params is present' do - get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, continue: { to: '/', notice_now: 'Started' } + get :show, namespace_id: project.namespace.to_param, project_id: project, continue: { to: '/', notice_now: 'Started' } expect(flash.now[:notice]).to eq 'Started' end @@ -39,13 +39,13 @@ describe Projects::ImportsController do end it 'renders template' do - get :show, namespace_id: project.namespace.to_param, project_id: project.to_param + get :show, namespace_id: project.namespace.to_param, project_id: project expect(response).to render_template :show end it 'sets flash.now if params is present' do - get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, continue: { to: '/', notice_now: 'In progress' } + get :show, namespace_id: project.namespace.to_param, project_id: project, continue: { to: '/', notice_now: 'In progress' } expect(flash.now[:notice]).to eq 'In progress' end @@ -57,7 +57,7 @@ describe Projects::ImportsController do end it 'redirects to new_namespace_project_import_path' do - get :show, namespace_id: project.namespace.to_param, project_id: project.to_param + get :show, namespace_id: project.namespace.to_param, project_id: project expect(response).to redirect_to new_namespace_project_import_path(project.namespace, project) end @@ -72,7 +72,7 @@ describe Projects::ImportsController do it 'redirects to namespace_project_path' do allow_any_instance_of(Project).to receive(:forked?).and_return(true) - get :show, namespace_id: project.namespace.to_param, project_id: project.to_param + get :show, namespace_id: project.namespace.to_param, project_id: project expect(flash[:notice]).to eq 'The project was successfully forked.' expect(response).to redirect_to namespace_project_path(project.namespace, project) @@ -81,7 +81,7 @@ describe Projects::ImportsController do context 'when project is external' do it 'redirects to namespace_project_path' do - get :show, namespace_id: project.namespace.to_param, project_id: project.to_param + get :show, namespace_id: project.namespace.to_param, project_id: project expect(flash[:notice]).to eq 'The project was successfully imported.' expect(response).to redirect_to namespace_project_path(project.namespace, project) @@ -97,7 +97,7 @@ describe Projects::ImportsController do end it 'redirects to params[:to]' do - get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, continue: params + get :show, namespace_id: project.namespace.to_param, project_id: project, continue: params expect(flash[:notice]).to eq params[:notice] expect(response).to redirect_to params[:to] @@ -111,7 +111,7 @@ describe Projects::ImportsController do end it 'redirects to namespace_project_path' do - get :show, namespace_id: project.namespace.to_param, project_id: project.to_param + get :show, namespace_id: project.namespace.to_param, project_id: project expect(response).to redirect_to namespace_project_path(project.namespace, project) end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 4b89381eb96..46c758b4654 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -12,7 +12,7 @@ describe Projects::IssuesController do 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 + get :index, namespace_id: project.namespace, project_id: project expect(response).to redirect_to('https://example.com/project') end @@ -24,14 +24,16 @@ describe Projects::IssuesController do project.team << [user, :developer] end + it_behaves_like "issuables list meta-data", :issue + it "returns index" do - get :index, namespace_id: project.namespace.path, project_id: project.path + get :index, namespace_id: project.namespace, project_id: project expect(response).to have_http_status(200) end it "returns 301 if request path doesn't match project path" do - get :index, namespace_id: project.namespace.path, project_id: project.path.upcase + get :index, namespace_id: project.namespace, project_id: project.path.upcase expect(response).to redirect_to(namespace_project_issues_path(project.namespace, project)) end @@ -40,7 +42,7 @@ describe Projects::IssuesController do project.issues_enabled = false project.save - get :index, namespace_id: project.namespace.path, project_id: project.path + get :index, namespace_id: project.namespace, project_id: project expect(response).to have_http_status(404) end @@ -48,7 +50,7 @@ describe Projects::IssuesController do controller.instance_variable_set(:@project, project) allow(project).to receive(:default_issues_tracker?).and_return(false) - get :index, namespace_id: project.namespace.path, project_id: project.path + get :index, namespace_id: project.namespace, project_id: project expect(response).to have_http_status(404) end end @@ -65,8 +67,8 @@ describe Projects::IssuesController do it 'redirects to last_page if page number is larger than number of pages' do get :index, - namespace_id: project.namespace.path.to_param, - project_id: project.path.to_param, + namespace_id: project.namespace.to_param, + project_id: project, page: (last_page + 1).to_param expect(response).to redirect_to(namespace_project_issues_path(page: last_page, state: controller.params[:state], scope: controller.params[:scope])) @@ -74,8 +76,8 @@ describe Projects::IssuesController do it 'redirects to specified page' do get :index, - namespace_id: project.namespace.path.to_param, - project_id: project.path.to_param, + namespace_id: project.namespace.to_param, + project_id: project, page: last_page.to_param expect(assigns(:issues).current_page).to eq(last_page) @@ -92,7 +94,7 @@ describe Projects::IssuesController do end it 'builds a new issue' do - get :new, namespace_id: project.namespace.path, project_id: project + get :new, namespace_id: project.namespace, project_id: project expect(assigns(:issue)).to be_a_new(Issue) end @@ -102,7 +104,7 @@ describe Projects::IssuesController do project_with_repository.team << [user, :developer] mr = create(:merge_request_with_diff_notes, source_project: project_with_repository) - get :new, namespace_id: project_with_repository.namespace.path, project_id: project_with_repository, merge_request_for_resolving_discussions: mr.iid + get :new, namespace_id: project_with_repository.namespace, project_id: project_with_repository, merge_request_for_resolving_discussions: mr.iid expect(assigns(:issue).title).not_to be_empty expect(assigns(:issue).description).not_to be_empty @@ -115,7 +117,7 @@ describe Projects::IssuesController do allow(project).to receive(:external_issue_tracker).and_return(external) controller.instance_variable_set(:@project, project) - get :new, namespace_id: project.namespace.path, project_id: project + get :new, namespace_id: project.namespace, project_id: project expect(response).to redirect_to('https://example.com/issues/new') end @@ -123,14 +125,16 @@ describe Projects::IssuesController do end describe 'PUT #update' do + before do + sign_in(user) + project.team << [user, :developer] + end + + it_behaves_like 'update invalid issuable', Issue + context 'when moving issue to another private project' do let(:another_project) { create(:empty_project, :private) } - before do - sign_in(user) - project.team << [user, :developer] - end - context 'when user has access to move issue' do before { another_project.team << [user, :reporter] } @@ -150,10 +154,117 @@ describe Projects::IssuesController do end end + context 'Akismet is enabled' do + let(:project) { create(:project_empty_repo, :public) } + + before do + stub_application_setting(recaptcha_enabled: true) + allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true) + end + + context 'when an issue is not identified as spam' do + before do + allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false) + allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(false) + end + + it 'normally updates the issue' do + expect { update_issue(title: 'Foo') }.to change { issue.reload.title }.to('Foo') + end + end + + context 'when an issue is identified as spam' do + before { allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) } + + context 'when captcha is not verified' do + def update_spam_issue + update_issue(title: 'Spam Title', description: 'Spam lives here') + end + + before { allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false) } + + it 'rejects an issue recognized as a spam' do + expect { update_spam_issue }.not_to change{ issue.reload.title } + end + + it 'rejects an issue recognized as a spam when recaptcha disabled' do + stub_application_setting(recaptcha_enabled: false) + + expect { update_spam_issue }.not_to change{ issue.reload.title } + end + + it 'creates a spam log' do + update_spam_issue + + spam_logs = SpamLog.all + + expect(spam_logs.count).to eq(1) + expect(spam_logs.first.title).to eq('Spam Title') + expect(spam_logs.first.recaptcha_verified).to be_falsey + end + + it 'renders verify template' do + update_spam_issue + + expect(response).to render_template(:verify) + end + end + + context 'when captcha is verified' do + let(:spammy_title) { 'Whatever' } + let!(:spam_logs) { create_list(:spam_log, 2, user: user, title: spammy_title) } + + def update_verified_issue + update_issue({ title: spammy_title }, + { spam_log_id: spam_logs.last.id, + recaptcha_verification: true }) + end + + before do + allow_any_instance_of(described_class).to receive(:verify_recaptcha) + .and_return(true) + end + + it 'redirect to issue page' do + update_verified_issue + + expect(response). + to redirect_to(namespace_project_issue_path(project.namespace, project, issue)) + end + + it 'accepts an issue after recaptcha is verified' do + expect{ update_verified_issue }.to change{ issue.reload.title }.to(spammy_title) + end + + it 'marks spam log as recaptcha_verified' do + expect { update_verified_issue }.to change { SpamLog.last.recaptcha_verified }.from(false).to(true) + end + + it 'does not mark spam log as recaptcha_verified when it does not belong to current_user' do + spam_log = create(:spam_log) + + expect { update_issue(spam_log_id: spam_log.id, recaptcha_verification: true) }. + not_to change { SpamLog.last.recaptcha_verified } + end + end + end + end + + def update_issue(issue_params = {}, additional_params = {}) + params = { + namespace_id: project.namespace.to_param, + project_id: project, + id: issue.iid, + issue: issue_params + }.merge(additional_params) + + put :update, params + end + def move_issue put :update, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, id: issue.iid, issue: { title: 'New title' }, move_to_project_id: another_project.id @@ -233,7 +344,7 @@ describe Projects::IssuesController do def get_issues get :index, namespace_id: project.namespace.to_param, - project_id: project.to_param + project_id: project end end @@ -296,7 +407,7 @@ describe Projects::IssuesController do def go(id:) get :show, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, id: id end end @@ -307,7 +418,7 @@ describe Projects::IssuesController do def go(id:) get :edit, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, id: id end end @@ -318,7 +429,7 @@ describe Projects::IssuesController do def go(id:) put :update, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, id: id, issue: { title: 'New title' } end @@ -333,7 +444,7 @@ describe Projects::IssuesController do post :create, { namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, issue: { title: 'Title', description: 'Description' }.merge(issue_attrs) }.merge(additional_params) @@ -355,7 +466,7 @@ describe Projects::IssuesController do end def post_issue(issue_params) - post :create, namespace_id: project.namespace.to_param, project_id: project.to_param, issue: issue_params, merge_request_for_resolving_discussions: merge_request.iid + post :create, namespace_id: project.namespace.to_param, project_id: project, issue: issue_params, merge_request_for_resolving_discussions: merge_request.iid end it 'creates an issue for the project' do @@ -382,7 +493,7 @@ describe Projects::IssuesController do allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true) end - context 'when an issue is not identified as a spam' do + context 'when an issue is not identified as spam' do before do allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false) allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(false) @@ -393,7 +504,7 @@ describe Projects::IssuesController do end end - context 'when an issue is identified as a spam' do + context 'when an issue is identified as spam' do before { allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) } context 'when captcha is not verified' do @@ -498,8 +609,8 @@ describe Projects::IssuesController do project.team << [admin, :master] sign_in(admin) post :mark_as_spam, { - namespace_id: project.namespace.path, - project_id: project.path, + namespace_id: project.namespace, + project_id: project, id: issue.iid } end @@ -515,7 +626,7 @@ describe Projects::IssuesController do context "when the user is a developer" do before { sign_in(user) } it "rejects a developer to destroy an issue" do - delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: issue.iid + delete :destroy, namespace_id: project.namespace, project_id: project, id: issue.iid expect(response).to have_http_status(404) end end @@ -528,7 +639,7 @@ describe Projects::IssuesController do before { sign_in(owner) } it "deletes the issue" do - delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: issue.iid + delete :destroy, namespace_id: project.namespace, project_id: project, id: issue.iid expect(response).to have_http_status(302) expect(controller).to set_flash[:notice].to(/The issue was successfully deleted\./).now @@ -537,7 +648,7 @@ describe Projects::IssuesController do 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 + delete :destroy, namespace_id: project.namespace, project_id: project, id: issue.iid end end end @@ -550,8 +661,8 @@ describe Projects::IssuesController do it "toggles the award emoji" do expect do - post(:toggle_award_emoji, namespace_id: project.namespace.path, - project_id: project.path, id: issue.iid, name: "thumbsup") + post(:toggle_award_emoji, namespace_id: project.namespace, + project_id: project, id: issue.iid, name: "thumbsup") end.to change { issue.award_emoji.count }.by(1) expect(response).to have_http_status(200) diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb index 3e0326dd47d..6a6e9bf378a 100644 --- a/spec/controllers/projects/labels_controller_spec.rb +++ b/spec/controllers/projects/labels_controller_spec.rb @@ -67,7 +67,7 @@ describe Projects::LabelsController do end def list_labels - get :index, namespace_id: project.namespace.to_param, project_id: project.to_param + get :index, namespace_id: project.namespace.to_param, project_id: project end end @@ -76,7 +76,7 @@ describe Projects::LabelsController do let(:personal_project) { create(:empty_project, namespace: user.namespace) } it 'creates labels' do - post :generate, namespace_id: personal_project.namespace.to_param, project_id: personal_project.to_param + post :generate, namespace_id: personal_project.namespace.to_param, project_id: personal_project expect(response).to have_http_status(302) end @@ -84,7 +84,7 @@ describe Projects::LabelsController do context 'project belonging to a group' do it 'creates labels' do - post :generate, namespace_id: project.namespace.to_param, project_id: project.to_param + post :generate, namespace_id: project.namespace.to_param, project_id: project expect(response).to have_http_status(302) end @@ -109,7 +109,7 @@ describe Projects::LabelsController do end def toggle_subscription(label) - post :toggle_subscription, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label.to_param + post :toggle_subscription, namespace_id: project.namespace.to_param, project_id: project, id: label.to_param end end @@ -119,7 +119,7 @@ describe Projects::LabelsController do context 'not group owner' do it 'denies access' do - post :promote, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label_1.to_param + post :promote, namespace_id: project.namespace.to_param, project_id: project, id: label_1.to_param expect(response).to have_http_status(404) end @@ -131,13 +131,13 @@ describe Projects::LabelsController do end it 'gives access' do - post :promote, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label_1.to_param + post :promote, namespace_id: project.namespace.to_param, project_id: project, id: label_1.to_param expect(response).to redirect_to(namespace_project_labels_path) end it 'promotes the label' do - post :promote, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label_1.to_param + post :promote, namespace_id: project.namespace.to_param, project_id: project, id: label_1.to_param expect(Label.where(id: label_1.id)).to be_empty expect(GroupLabel.find_by(title: promoted_label_name)).not_to be_nil @@ -151,7 +151,7 @@ describe Projects::LabelsController do end it 'returns to label list' do - post :promote, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label_1.to_param + post :promote, namespace_id: project.namespace.to_param, project_id: project, id: label_1.to_param expect(response).to redirect_to(namespace_project_labels_path) end end diff --git a/spec/controllers/projects/mattermosts_controller_spec.rb b/spec/controllers/projects/mattermosts_controller_spec.rb index cae733f0cfb..c5abf11cfa5 100644 --- a/spec/controllers/projects/mattermosts_controller_spec.rb +++ b/spec/controllers/projects/mattermosts_controller_spec.rb @@ -18,7 +18,7 @@ describe Projects::MattermostsController do it 'accepts the request' do get(:new, namespace_id: project.namespace.to_param, - project_id: project.to_param) + project_id: project) expect(response).to have_http_status(200) end @@ -30,7 +30,7 @@ describe Projects::MattermostsController do subject do post(:create, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, mattermost: mattermost_params) end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 63780802cfa..250d64f7055 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -43,7 +43,8 @@ describe Projects::MergeRequestsController do submit_new_merge_request(format: :json) expect(response).to be_ok - expect(json_response).not_to be_empty + expect(json_response).to have_key 'pipelines' + expect(json_response['pipelines']).not_to be_empty end end end @@ -51,10 +52,11 @@ describe Projects::MergeRequestsController do def submit_new_merge_request(format: :html) get :new, namespace_id: fork_project.namespace.to_param, - project_id: fork_project.to_param, + project_id: fork_project, merge_request: { source_branch: 'remove-submodule', - target_branch: 'master' }, + target_branch: 'master' + }, format: format end end @@ -63,7 +65,7 @@ describe Projects::MergeRequestsController do it "loads labels into the @labels variable" do get action, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, id: merge_request.iid, format: 'html' expect(assigns(:labels)).not_to be_nil @@ -75,7 +77,7 @@ describe Projects::MergeRequestsController do it "does generally work" do get(:show, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, id: merge_request.iid, format: format) @@ -89,7 +91,7 @@ describe Projects::MergeRequestsController do get(:show, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, id: merge_request.iid, format: format) end @@ -97,7 +99,7 @@ describe Projects::MergeRequestsController do it "renders it" do get(:show, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, id: merge_request.iid, format: format) @@ -110,7 +112,7 @@ describe Projects::MergeRequestsController do get(:show, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, id: merge_request.iid, format: format) @@ -125,7 +127,7 @@ describe Projects::MergeRequestsController do it "triggers workhorse to serve the request" do get(:show, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, id: merge_request.iid, format: :diff) @@ -137,7 +139,7 @@ describe Projects::MergeRequestsController do it 'triggers workhorse to serve the request' do get(:show, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, id: merge_request.iid, format: :patch) @@ -147,13 +149,17 @@ describe Projects::MergeRequestsController do end describe 'GET index' do + let!(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } + def get_merge_requests(page = nil) get :index, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, state: 'opened', page: page.to_param end + it_behaves_like "issuables list meta-data", :merge_request + context 'when page param' do let(:last_page) { project.merge_requests.page().total_pages } let!(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } @@ -211,8 +217,8 @@ describe Projects::MergeRequestsController do it 'closes MR without errors' do post :update, - namespace_id: project.namespace.path, - project_id: project.path, + namespace_id: project.namespace, + project_id: project, id: merge_request.iid, merge_request: { state_event: 'close' @@ -226,8 +232,8 @@ describe Projects::MergeRequestsController do merge_request.close! put :update, - namespace_id: project.namespace.path, - project_id: project.path, + namespace_id: project.namespace, + project_id: project, id: merge_request.iid, merge_request: { title: 'New title' @@ -241,8 +247,8 @@ describe Projects::MergeRequestsController do merge_request.close! put :update, - namespace_id: project.namespace.path, - project_id: project.path, + namespace_id: project.namespace, + project_id: project, id: merge_request.iid, merge_request: { target_branch: 'new_branch' @@ -250,14 +256,16 @@ describe Projects::MergeRequestsController do expect { merge_request.reload.target_branch }.not_to change { merge_request.target_branch } end + + it_behaves_like 'update invalid issuable', MergeRequest end end describe 'POST merge' do let(:base_params) do { - namespace_id: project.namespace.path, - project_id: project.path, + namespace_id: project.namespace, + project_id: project, id: merge_request.iid, format: 'raw' } @@ -312,41 +320,41 @@ describe Projects::MergeRequestsController do merge_with_sha end - context 'when merge_when_build_succeeds is passed' do - def merge_when_build_succeeds - post :merge, base_params.merge(sha: merge_request.diff_head_sha, merge_when_build_succeeds: '1') + context 'when the pipeline succeeds is passed' do + def merge_when_pipeline_succeeds + post :merge, base_params.merge(sha: merge_request.diff_head_sha, merge_when_pipeline_succeeds: '1') end before do create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch) end - it 'returns :merge_when_build_succeeds' do - merge_when_build_succeeds + it 'returns :merge_when_pipeline_succeeds' do + merge_when_pipeline_succeeds - expect(assigns(:status)).to eq(:merge_when_build_succeeds) + expect(assigns(:status)).to eq(:merge_when_pipeline_succeeds) end - it 'sets the MR to merge when the build succeeds' do - service = double(:merge_when_build_succeeds_service) + it 'sets the MR to merge when the pipeline succeeds' do + service = double(:merge_when_pipeline_succeeds_service) expect(MergeRequests::MergeWhenPipelineSucceedsService) .to receive(:new).with(project, anything, anything) .and_return(service) expect(service).to receive(:execute).with(merge_request) - merge_when_build_succeeds + merge_when_pipeline_succeeds end - context 'when project.only_allow_merge_if_build_succeeds? is true' do + context 'when project.only_allow_merge_if_pipeline_succeeds? is true' do before do - project.update_column(:only_allow_merge_if_build_succeeds, true) + project.update_column(:only_allow_merge_if_pipeline_succeeds, true) end - it 'returns :merge_when_build_succeeds' do - merge_when_build_succeeds + it 'returns :merge_when_pipeline_succeeds' do + merge_when_pipeline_succeeds - expect(assigns(:status)).to eq(:merge_when_build_succeeds) + expect(assigns(:status)).to eq(:merge_when_pipeline_succeeds) end end end @@ -421,7 +429,7 @@ describe Projects::MergeRequestsController do describe "DELETE destroy" do it "denies access to users unless they're admin or project owner" do - delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid + delete :destroy, namespace_id: project.namespace, project_id: project, id: merge_request.iid expect(response).to have_http_status(404) end @@ -434,7 +442,7 @@ describe Projects::MergeRequestsController do before { sign_in owner } it "deletes the merge request" do - delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid + delete :destroy, namespace_id: project.namespace, project_id: project, id: merge_request.iid expect(response).to have_http_status(302) expect(controller).to set_flash[:notice].to(/The merge request was successfully deleted\./).now @@ -443,7 +451,7 @@ describe Projects::MergeRequestsController do 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 + delete :destroy, namespace_id: project.namespace, project_id: project, id: merge_request.iid end end end @@ -452,7 +460,7 @@ describe Projects::MergeRequestsController do def go(extra_params = {}) params = { namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, id: merge_request.iid } @@ -532,7 +540,7 @@ describe Projects::MergeRequestsController do def diff_for_path(extra_params = {}) params = { namespace_id: project.namespace.to_param, - project_id: project.to_param + project_id: project } get :diff_for_path, params.merge(extra_params) @@ -596,7 +604,7 @@ describe Projects::MergeRequestsController do before do other_project.team << [user, :master] - diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path, project_id: other_project.to_param) + diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path, project_id: other_project) end it 'returns a 404' do @@ -662,7 +670,7 @@ describe Projects::MergeRequestsController do def go(format: 'html') get :commits, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, id: merge_request.iid, format: format end @@ -702,7 +710,7 @@ describe Projects::MergeRequestsController do before do get :pipelines, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, id: merge_request.iid, format: :json end @@ -721,7 +729,7 @@ describe Projects::MergeRequestsController do get :conflicts, namespace_id: merge_request_with_conflicts.project.namespace.to_param, - project_id: merge_request_with_conflicts.project.to_param, + project_id: merge_request_with_conflicts.project, id: merge_request_with_conflicts.iid, format: 'json' end @@ -739,7 +747,7 @@ describe Projects::MergeRequestsController do before do get :conflicts, namespace_id: merge_request_with_conflicts.project.namespace.to_param, - project_id: merge_request_with_conflicts.project.to_param, + project_id: merge_request_with_conflicts.project, id: merge_request_with_conflicts.iid, format: 'json' end @@ -768,7 +776,7 @@ describe Projects::MergeRequestsController do section['lines'].each do |line| if section['conflict'] - expect(line['type']).to be_in(['old', 'new']) + expect(line['type']).to be_in(%w(old new)) expect(line.values_at('old_line', 'new_line')).to contain_exactly(nil, a_kind_of(Integer)) else if line['type'].nil? @@ -802,7 +810,7 @@ describe Projects::MergeRequestsController do post :remove_wip, namespace_id: merge_request.project.namespace.to_param, - project_id: merge_request.project.to_param, + project_id: merge_request.project, id: merge_request.iid expect(merge_request.reload.title).to eq(merge_request.wipless_title) @@ -813,7 +821,7 @@ describe Projects::MergeRequestsController do def conflict_for_path(path) get :conflict_for_path, namespace_id: merge_request_with_conflicts.project.namespace.to_param, - project_id: merge_request_with_conflicts.project.to_param, + project_id: merge_request_with_conflicts.project, id: merge_request_with_conflicts.iid, old_path: path, new_path: path, @@ -869,7 +877,7 @@ describe Projects::MergeRequestsController do def resolve_conflicts(files) post :resolve_conflicts, namespace_id: merge_request_with_conflicts.project.namespace.to_param, - project_id: merge_request_with_conflicts.project.to_param, + project_id: merge_request_with_conflicts.project, id: merge_request_with_conflicts.iid, format: 'json', files: files, @@ -1020,7 +1028,7 @@ describe Projects::MergeRequestsController do post :assign_related_issues, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, id: merge_request.iid end @@ -1075,7 +1083,7 @@ describe Projects::MergeRequestsController do get :ci_environments_status, namespace_id: merge_request.project.namespace.to_param, - project_id: merge_request.project.to_param, + project_id: merge_request.project, id: merge_request.iid, format: 'json' end @@ -1088,8 +1096,8 @@ describe Projects::MergeRequestsController do describe 'GET merge_widget_refresh' do let(:params) do { - namespace_id: project.namespace.path, - project_id: project.path, + namespace_id: project.namespace, + project_id: project, id: merge_request.iid, format: :raw } @@ -1127,27 +1135,27 @@ describe Projects::MergeRequestsController do end context 'when waiting for build' do - let(:merge_request) { create(:merge_request, source_project: project, merge_when_build_succeeds: true, merge_user: user) } + let(:merge_request) { create(:merge_request, source_project: project, merge_when_pipeline_succeeds: true, merge_user: user) } it 'returns an OK response' do expect(response).to have_http_status(:ok) end - it 'sets status to :merge_when_build_succeeds' do - expect(assigns(:status)).to eq(:merge_when_build_succeeds) + it 'sets status to :merge_when_pipeline_succeeds' do + expect(assigns(:status)).to eq(:merge_when_pipeline_succeeds) expect(response).to render_template('merge') end end - context 'when no special status for MR' do + context 'when MR does not have special state' do let(:merge_request) { create(:merge_request, source_project: project) } it 'returns an OK response' do expect(response).to have_http_status(:ok) end - it 'sets status to nil' do - expect(assigns(:status)).to be_nil + it 'sets status to success' do + expect(assigns(:status)).to eq(:success) expect(response).to render_template('merge') end end diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index 1ed2ee3ab4a..04bb5cbbd59 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -12,10 +12,13 @@ describe Projects::PipelinesController do describe 'GET index.json' do before do - create_list(:ci_empty_pipeline, 2, project: project) + create(:ci_empty_pipeline, status: 'pending', project: project) + create(:ci_empty_pipeline, status: 'running', project: project) + create(:ci_empty_pipeline, status: 'created', project: project) + create(:ci_empty_pipeline, status: 'success', project: project) - get :index, namespace_id: project.namespace.path, - project_id: project.path, + get :index, namespace_id: project.namespace, + project_id: project, format: :json end @@ -23,9 +26,11 @@ describe Projects::PipelinesController do expect(response).to have_http_status(:ok) expect(json_response).to include('pipelines') - expect(json_response['pipelines'].count).to eq 2 - expect(json_response['count']['all']).to eq 2 - expect(json_response['count']['running_or_pending']).to eq 2 + expect(json_response['pipelines'].count).to eq 4 + expect(json_response['count']['all']).to eq 4 + expect(json_response['count']['running']).to eq 1 + expect(json_response['count']['pending']).to eq 1 + expect(json_response['count']['finished']).to eq 1 end end @@ -57,8 +62,8 @@ describe Projects::PipelinesController do end def get_stage(name) - get :stage, namespace_id: project.namespace.path, - project_id: project.path, + get :stage, namespace_id: project.namespace, + project_id: project, id: pipeline.id, stage: name, format: :json diff --git a/spec/controllers/projects/protected_branches_controller_spec.rb b/spec/controllers/projects/protected_branches_controller_spec.rb index da6112a13f7..e378b5714fe 100644 --- a/spec/controllers/projects/protected_branches_controller_spec.rb +++ b/spec/controllers/projects/protected_branches_controller_spec.rb @@ -4,7 +4,7 @@ describe Projects::ProtectedBranchesController do describe "GET #index" do let(:project) { create(:project_empty_repo, :public) } it "redirects empty repo to projects page" do - get(:index, namespace_id: project.namespace.to_param, project_id: project.to_param) + get(:index, namespace_id: project.namespace.to_param, project_id: project) end end end diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb index b23d6e257ba..4cebe3884bf 100644 --- a/spec/controllers/projects/raw_controller_spec.rb +++ b/spec/controllers/projects/raw_controller_spec.rb @@ -10,7 +10,7 @@ describe Projects::RawController do it 'delivers ASCII file' do get(:show, namespace_id: public_project.namespace.to_param, - project_id: public_project.to_param, + project_id: public_project, id: id) expect(response).to have_http_status(200) @@ -27,7 +27,7 @@ describe Projects::RawController do it 'sets image content type header' do get(:show, namespace_id: public_project.namespace.to_param, - project_id: public_project.to_param, + project_id: public_project, id: id) expect(response).to have_http_status(200) @@ -51,7 +51,7 @@ describe Projects::RawController do expect(controller).to receive(:send_file).with("#{Gitlab.config.shared.path}/lfs-objects/91/ef/f75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897", filename: "lfs_object.iso", disposition: 'attachment') get(:show, namespace_id: public_project.namespace.to_param, - project_id: public_project.to_param, + project_id: public_project, id: id) expect(response).to have_http_status(200) @@ -62,7 +62,7 @@ describe Projects::RawController do it 'does not serve the file' do get(:show, namespace_id: public_project.namespace.to_param, - project_id: public_project.to_param, + project_id: public_project, id: id) expect(response).to have_http_status(404) diff --git a/spec/controllers/projects/refs_controller_spec.rb b/spec/controllers/projects/refs_controller_spec.rb index d8fb4667c67..3a3e7467ef2 100644 --- a/spec/controllers/projects/refs_controller_spec.rb +++ b/spec/controllers/projects/refs_controller_spec.rb @@ -13,7 +13,7 @@ describe Projects::RefsController do def default_get(format = :html) get :logs_tree, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, id: 'master', path: 'foo/bar/baz.html', format: format @@ -23,7 +23,7 @@ describe Projects::RefsController do xhr :get, :logs_tree, namespace_id: project.namespace.to_param, - project_id: project.to_param, id: 'master', + project_id: project, id: 'master', path: 'foo/bar/baz.html', format: format end diff --git a/spec/controllers/projects/releases_controller_spec.rb b/spec/controllers/projects/releases_controller_spec.rb index 69fcc26c77e..358f26dfb02 100644 --- a/spec/controllers/projects/releases_controller_spec.rb +++ b/spec/controllers/projects/releases_controller_spec.rb @@ -16,7 +16,7 @@ describe Projects::ReleasesController do tag_id = release.tag project.releases.destroy_all - get :edit, namespace_id: project.namespace.path, project_id: project.path, tag_id: tag_id + get :edit, namespace_id: project.namespace, project_id: project, tag_id: tag_id release = assigns(:release) expect(release).not_to be_nil @@ -24,7 +24,7 @@ describe Projects::ReleasesController do end it 'retrieves an existing release' do - get :edit, namespace_id: project.namespace.path, project_id: project.path, tag_id: release.tag + get :edit, namespace_id: project.namespace, project_id: project, tag_id: release.tag release = assigns(:release) expect(release).not_to be_nil @@ -48,7 +48,7 @@ describe Projects::ReleasesController do def update_release(description) put :update, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, tag_id: release.tag, release: { description: description } end diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb index 04e88879fb8..9c55d159fa0 100644 --- a/spec/controllers/projects/repositories_controller_spec.rb +++ b/spec/controllers/projects/repositories_controller_spec.rb @@ -6,7 +6,7 @@ describe Projects::RepositoriesController do describe "GET archive" do context 'as a guest' do it 'responds with redirect in correct format' do - get :archive, namespace_id: project.namespace.path, project_id: project.path, format: "zip" + get :archive, namespace_id: project.namespace, project_id: project, format: "zip" expect(response.header["Content-Type"]).to start_with('text/html') expect(response).to be_redirect @@ -22,7 +22,7 @@ describe Projects::RepositoriesController do end it "uses Gitlab::Workhorse" do - get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip" + get :archive, namespace_id: project.namespace, project_id: project, ref: "master", format: "zip" expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:") end @@ -33,7 +33,7 @@ describe Projects::RepositoriesController do end it "renders Not Found" do - get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip" + get :archive, namespace_id: project.namespace, project_id: project, ref: "master", format: "zip" expect(response).to have_http_status(404) end diff --git a/spec/controllers/projects/runners_controller_spec.rb b/spec/controllers/projects/runners_controller_spec.rb new file mode 100644 index 00000000000..0fa249e4405 --- /dev/null +++ b/spec/controllers/projects/runners_controller_spec.rb @@ -0,0 +1,75 @@ +require 'spec_helper' + +describe Projects::RunnersController do + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + let(:runner) { create(:ci_runner) } + + let(:params) do + { + namespace_id: project.namespace, + project_id: project, + id: runner + } + end + + before do + sign_in(user) + project.add_master(user) + project.runners << runner + end + + describe '#update' do + it 'updates the runner and ticks the queue' do + new_desc = runner.description.swapcase + + expect do + post :update, params.merge(runner: { description: new_desc } ) + end.to change { runner.ensure_runner_queue_value } + + runner.reload + + expect(response).to have_http_status(302) + expect(runner.description).to eq(new_desc) + end + end + + describe '#destroy' do + it 'destroys the runner' do + delete :destroy, params + + expect(response).to have_http_status(302) + expect(Ci::Runner.find_by(id: runner.id)).to be_nil + end + end + + describe '#resume' do + it 'marks the runner as active and ticks the queue' do + runner.update(active: false) + + expect do + post :resume, params + end.to change { runner.ensure_runner_queue_value } + + runner.reload + + expect(response).to have_http_status(302) + expect(runner.active).to eq(true) + end + end + + describe '#pause' do + it 'marks the runner as inactive and ticks the queue' do + runner.update(active: true) + + expect do + post :pause, params + end.to change { runner.ensure_runner_queue_value } + + runner.reload + + expect(response).to have_http_status(302) + expect(runner.active).to eq(false) + end + end +end diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb index 19e948d8fb8..24a59caff4e 100644 --- a/spec/controllers/projects/snippets_controller_spec.rb +++ b/spec/controllers/projects/snippets_controller_spec.rb @@ -17,16 +17,16 @@ describe Projects::SnippetsController do it 'redirects to last_page if page number is larger than number of pages' do get :index, - namespace_id: project.namespace.path, - project_id: project.path, page: (last_page + 1).to_param + namespace_id: project.namespace, + project_id: project, page: (last_page + 1).to_param expect(response).to redirect_to(namespace_project_snippets_path(page: last_page)) end it 'redirects to specified page' do get :index, - namespace_id: project.namespace.path, - project_id: project.path, page: last_page.to_param + namespace_id: project.namespace, + project_id: project, page: last_page.to_param expect(assigns(:snippets).current_page).to eq(last_page) expect(response).to have_http_status(200) @@ -38,7 +38,7 @@ describe Projects::SnippetsController do context 'when anonymous' do it 'does not include the private snippet' do - get :index, namespace_id: project.namespace.path, project_id: project.path + get :index, namespace_id: project.namespace, project_id: project expect(assigns(:snippets)).not_to include(project_snippet) expect(response).to have_http_status(200) @@ -49,7 +49,7 @@ describe Projects::SnippetsController do before { sign_in(user) } it 'renders the snippet' do - get :index, namespace_id: project.namespace.path, project_id: project.path + get :index, namespace_id: project.namespace, project_id: project expect(assigns(:snippets)).to include(project_snippet) expect(response).to have_http_status(200) @@ -60,7 +60,7 @@ describe Projects::SnippetsController do before { sign_in(user2) } it 'renders the snippet' do - get :index, namespace_id: project.namespace.path, project_id: project.path + get :index, namespace_id: project.namespace, project_id: project expect(assigns(:snippets)).to include(project_snippet) expect(response).to have_http_status(200) @@ -70,16 +70,16 @@ describe Projects::SnippetsController do end describe 'POST #create' do - def create_snippet(project, snippet_params = {}) + def create_snippet(project, snippet_params = {}, additional_params = {}) sign_in(user) project.add_developer(user) post :create, { namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, project_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params) - } + }.merge(additional_params) end context 'when the snippet is spam' do @@ -87,35 +87,179 @@ describe Projects::SnippetsController do allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) end - context 'when the project is private' do - let(:private_project) { create(:project_empty_repo, :private) } + context 'when the snippet is private' do + it 'creates the snippet' do + expect { create_snippet(project, visibility_level: Snippet::PRIVATE) }. + to change { Snippet.count }.by(1) + end + end + + context 'when the snippet is public' do + it 'rejects the shippet' do + expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }. + not_to change { Snippet.count } + expect(response).to render_template(:new) + end + + it 'creates a spam log' do + expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }. + to change { SpamLog.count }.by(1) + end - context 'when the snippet is public' do - it 'creates the snippet' do - expect { create_snippet(private_project, visibility_level: Snippet::PUBLIC) }. - to change { Snippet.count }.by(1) + it 'renders :new with recaptcha disabled' do + stub_application_setting(recaptcha_enabled: false) + + create_snippet(project, visibility_level: Snippet::PUBLIC) + + expect(response).to render_template(:new) + end + + context 'recaptcha enabled' do + before do + stub_application_setting(recaptcha_enabled: true) + end + + it 'renders :verify with recaptcha enabled' do + create_snippet(project, visibility_level: Snippet::PUBLIC) + + expect(response).to render_template(:verify) + end + + it 'renders snippet page when recaptcha verified' do + spammy_title = 'Whatever' + + spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title) + create_snippet(project, + { visibility_level: Snippet::PUBLIC }, + { spam_log_id: spam_logs.last.id, + recaptcha_verification: true }) + + expect(response).to redirect_to(Snippet.last) end end end + end + end + + describe 'PUT #update' do + let(:project) { create :project, :public } + let(:snippet) { create :project_snippet, author: user, project: project, visibility_level: visibility_level } + + def update_snippet(snippet_params = {}, additional_params = {}) + sign_in(user) + + project.add_developer(user) + + put :update, { + namespace_id: project.namespace.to_param, + project_id: project, + id: snippet.id, + project_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params) + }.merge(additional_params) + + snippet.reload + end + + context 'when the snippet is spam' do + before do + allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) + end + + context 'when the snippet is private' do + let(:visibility_level) { Snippet::PRIVATE } + + it 'updates the snippet' do + expect { update_snippet(title: 'Foo') }. + to change { snippet.reload.title }.to('Foo') + end + end + + context 'when the snippet is public' do + let(:visibility_level) { Snippet::PUBLIC } + + it 'rejects the shippet' do + expect { update_snippet(title: 'Foo') }. + not_to change { snippet.reload.title } + end - context 'when the project is public' do - context 'when the snippet is private' do - it 'creates the snippet' do - expect { create_snippet(project, visibility_level: Snippet::PRIVATE) }. - to change { Snippet.count }.by(1) + it 'creates a spam log' do + expect { update_snippet(title: 'Foo') }. + to change { SpamLog.count }.by(1) + end + + it 'renders :edit with recaptcha disabled' do + stub_application_setting(recaptcha_enabled: false) + + update_snippet(title: 'Foo') + + expect(response).to render_template(:edit) + end + + context 'recaptcha enabled' do + before do + stub_application_setting(recaptcha_enabled: true) + end + + it 'renders :verify with recaptcha enabled' do + update_snippet(title: 'Foo') + + expect(response).to render_template(:verify) + end + + it 'renders snippet page when recaptcha verified' do + spammy_title = 'Whatever' + + spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title) + snippet = update_snippet({ title: spammy_title }, + { spam_log_id: spam_logs.last.id, + recaptcha_verification: true }) + + expect(response).to redirect_to(snippet) end end + end + + context 'when the private snippet is made public' do + let(:visibility_level) { Snippet::PRIVATE } + + it 'rejects the shippet' do + expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }. + not_to change { snippet.reload.title } + end + + it 'creates a spam log' do + expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }. + to change { SpamLog.count }.by(1) + end + + it 'renders :edit with recaptcha disabled' do + stub_application_setting(recaptcha_enabled: false) + + update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) + + expect(response).to render_template(:edit) + end + + context 'recaptcha enabled' do + before do + stub_application_setting(recaptcha_enabled: true) + end - context 'when the snippet is public' do - it 'rejects the shippet' do - expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }. - not_to change { Snippet.count } - expect(response).to render_template(:new) + it 'renders :verify with recaptcha enabled' do + update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) + + expect(response).to render_template(:verify) end - it 'creates a spam log' do - expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }. - to change { SpamLog.count }.by(1) + it 'renders snippet page when recaptcha verified' do + spammy_title = 'Whatever' + + spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title) + snippet = update_snippet({ title: spammy_title, visibility_level: Snippet::PUBLIC }, + { spam_log_id: spam_logs.last.id, + recaptcha_verification: true }) + + expect(response).to redirect_to(snippet) end end end @@ -137,8 +281,8 @@ describe Projects::SnippetsController do sign_in(admin) post :mark_as_spam, - namespace_id: project.namespace.path, - project_id: project.path, + namespace_id: project.namespace, + project_id: project, id: snippet.id end @@ -156,7 +300,7 @@ describe Projects::SnippetsController do context 'when anonymous' do it 'responds with status 404' do - get action, namespace_id: project.namespace.path, project_id: project.path, id: project_snippet.to_param + get action, namespace_id: project.namespace, project_id: project, id: project_snippet.to_param expect(response).to have_http_status(404) end @@ -166,7 +310,7 @@ describe Projects::SnippetsController do before { sign_in(user) } it 'renders the snippet' do - get action, namespace_id: project.namespace.path, project_id: project.path, id: project_snippet.to_param + get action, namespace_id: project.namespace, project_id: project, id: project_snippet.to_param expect(assigns(:snippet)).to eq(project_snippet) expect(response).to have_http_status(200) @@ -177,7 +321,7 @@ describe Projects::SnippetsController do before { sign_in(user2) } it 'renders the snippet' do - get action, namespace_id: project.namespace.path, project_id: project.path, id: project_snippet.to_param + get action, namespace_id: project.namespace, project_id: project, id: project_snippet.to_param expect(assigns(:snippet)).to eq(project_snippet) expect(response).to have_http_status(200) @@ -188,7 +332,7 @@ describe Projects::SnippetsController do context 'when the project snippet does not exist' do context 'when anonymous' do it 'responds with status 404' do - get action, namespace_id: project.namespace.path, project_id: project.path, id: 42 + get action, namespace_id: project.namespace, project_id: project, id: 42 expect(response).to have_http_status(404) end @@ -198,7 +342,7 @@ describe Projects::SnippetsController do before { sign_in(user) } it 'responds with status 404' do - get action, namespace_id: project.namespace.path, project_id: project.path, id: 42 + get action, namespace_id: project.namespace, project_id: project, id: 42 expect(response).to have_http_status(404) end @@ -206,4 +350,37 @@ describe Projects::SnippetsController do end end end + + describe 'GET #raw' do + let(:project_snippet) do + create( + :project_snippet, :public, + project: project, + author: user, + content: "first line\r\nsecond line\r\nthird line" + ) + end + + context 'CRLF line ending' do + let(:params) do + { + namespace_id: project.namespace, + project_id: project, + id: project_snippet.to_param + } + end + + it 'returns LF line endings by default' do + get :raw, params + + expect(response.body).to eq("first line\nsecond line\nthird line") + end + + it 'does not convert line endings when parameter present' do + get :raw, params.merge(line_ending: :raw) + + expect(response.body).to eq("first line\r\nsecond line\r\nthird line") + end + end + end end diff --git a/spec/controllers/projects/tags_controller_spec.rb b/spec/controllers/projects/tags_controller_spec.rb index c36a5fdd66c..fc97bac64cd 100644 --- a/spec/controllers/projects/tags_controller_spec.rb +++ b/spec/controllers/projects/tags_controller_spec.rb @@ -6,7 +6,7 @@ describe Projects::TagsController do let!(:invalid_release) { create(:release, project: project, tag: 'does-not-exist') } describe 'GET index' do - before { get :index, namespace_id: project.namespace.to_param, project_id: project.to_param } + before { get :index, namespace_id: project.namespace.to_param, project_id: project } it 'returns the tags for the page' do expect(assigns(:tags).map(&:name)).to eq(['v1.1.0', 'v1.0.0']) @@ -19,7 +19,7 @@ describe Projects::TagsController do end describe 'GET show' do - before { get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, id: id } + before { get :show, namespace_id: project.namespace.to_param, project_id: project, id: id } context "valid tag" do let(:id) { 'v1.0.0' } diff --git a/spec/controllers/projects/templates_controller_spec.rb b/spec/controllers/projects/templates_controller_spec.rb index 80f84a388ce..70e7f9ca96e 100644 --- a/spec/controllers/projects/templates_controller_spec.rb +++ b/spec/controllers/projects/templates_controller_spec.rb @@ -14,13 +14,13 @@ describe Projects::TemplatesController do before do project.add_user(user, Gitlab::Access::MASTER) - project.repository.commit_file(user, file_path_1, 'something valid', - message: 'test 3', branch_name: 'master', update: false) + project.repository.create_file(user, file_path_1, 'something valid', + message: 'test 3', branch_name: 'master') end describe '#show' do it 'renders template name and content as json' do - get(:show, namespace_id: project.namespace.to_param, template_type: "issue", key: "bug", project_id: project.path, format: :json) + get(:show, namespace_id: project.namespace.to_param, template_type: "issue", key: "bug", project_id: project, format: :json) expect(response.status).to eq(200) expect(body["name"]).to eq("bug") @@ -29,21 +29,21 @@ describe Projects::TemplatesController do it 'renders 404 when unauthorized' do sign_in(user2) - get(:show, namespace_id: project.namespace.to_param, template_type: "issue", key: "bug", project_id: project.path, format: :json) + get(:show, namespace_id: project.namespace.to_param, template_type: "issue", key: "bug", project_id: project, format: :json) expect(response.status).to eq(404) end it 'renders 404 when template type is not found' do sign_in(user) - get(:show, namespace_id: project.namespace.to_param, template_type: "dont_exist", key: "bug", project_id: project.path, format: :json) + get(:show, namespace_id: project.namespace.to_param, template_type: "dont_exist", key: "bug", project_id: project, format: :json) expect(response.status).to eq(404) end it 'renders 404 without errors' do sign_in(user) - expect { get(:show, namespace_id: project.namespace.to_param, template_type: "dont_exist", key: "bug", project_id: project.path, format: :json) }.not_to raise_error + expect { get(:show, namespace_id: project.namespace.to_param, template_type: "dont_exist", key: "bug", project_id: project, format: :json) }.not_to raise_error end end end diff --git a/spec/controllers/projects/todo_controller_spec.rb b/spec/controllers/projects/todo_controller_spec.rb index 415c264e0dd..9a7beeff6fe 100644 --- a/spec/controllers/projects/todo_controller_spec.rb +++ b/spec/controllers/projects/todo_controller_spec.rb @@ -12,8 +12,8 @@ describe Projects::TodosController do describe 'POST create' do def go post :create, - namespace_id: project.namespace.path, - project_id: project.path, + namespace_id: project.namespace, + project_id: project, issuable_id: issue.id, issuable_type: 'issue', format: 'html' @@ -80,8 +80,8 @@ describe Projects::TodosController do describe 'POST create' do def go post :create, - namespace_id: project.namespace.path, - project_id: project.path, + namespace_id: project.namespace, + project_id: project, issuable_id: merge_request.id, issuable_type: 'merge_request', format: 'html' diff --git a/spec/controllers/projects/tree_controller_spec.rb b/spec/controllers/projects/tree_controller_spec.rb index b81645a3d2d..ab94e292e48 100644 --- a/spec/controllers/projects/tree_controller_spec.rb +++ b/spec/controllers/projects/tree_controller_spec.rb @@ -18,7 +18,7 @@ describe Projects::TreeController do before do get(:show, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, id: id) end @@ -74,7 +74,7 @@ describe Projects::TreeController do before do get(:show, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, id: id) end @@ -94,7 +94,7 @@ describe Projects::TreeController do before do post(:create_dir, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, id: 'master', dir_name: path, target_branch: target_branch, diff --git a/spec/controllers/projects/uploads_controller_spec.rb b/spec/controllers/projects/uploads_controller_spec.rb index f1c8891e87a..699c6f77cec 100644 --- a/spec/controllers/projects/uploads_controller_spec.rb +++ b/spec/controllers/projects/uploads_controller_spec.rb @@ -16,7 +16,7 @@ describe Projects::UploadsController do it "returns an error" do post :create, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, format: :json expect(response).to have_http_status(422) end @@ -26,7 +26,7 @@ describe Projects::UploadsController do before do post :create, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, file: jpg, format: :json end @@ -41,7 +41,7 @@ describe Projects::UploadsController do before do post :create, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, file: txt, format: :json end @@ -57,7 +57,7 @@ describe Projects::UploadsController do let(:go) do get :show, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, secret: "123456", filename: "image.jpg" end @@ -170,68 +170,24 @@ describe Projects::UploadsController do project.team << [user, :master] end - context "when the user is blocked" do + context "when the file exists" do before do - user.block - project.team << [user, :master] - end - - context "when the file exists" do - before do - allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg) - allow(jpg).to receive(:exists?).and_return(true) - end - - context "when the file is an image" do - before do - allow_any_instance_of(FileUploader).to receive(:image?).and_return(true) - end - - it "responds with status 200" do - go - - expect(response).to have_http_status(200) - end - end - - context "when the file is not an image" do - it "redirects to the sign in page" do - go - - expect(response).to redirect_to(new_user_session_path) - end - end + allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg) + allow(jpg).to receive(:exists?).and_return(true) end - context "when the file doesn't exist" do - it "redirects to the sign in page" do - go + it "responds with status 200" do + go - expect(response).to redirect_to(new_user_session_path) - end + expect(response).to have_http_status(200) end end - context "when the user isn't blocked" do - context "when the file exists" do - before do - allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg) - allow(jpg).to receive(:exists?).and_return(true) - end - - it "responds with status 200" do - go - - expect(response).to have_http_status(200) - end - end - - context "when the file doesn't exist" do - it "responds with status 404" do - go + context "when the file doesn't exist" do + it "responds with status 404" do + go - expect(response).to have_http_status(404) - end + expect(response).to have_http_status(404) end end end diff --git a/spec/controllers/projects/variables_controller_spec.rb b/spec/controllers/projects/variables_controller_spec.rb index 9fa358f7d62..e3f3b4fe8eb 100644 --- a/spec/controllers/projects/variables_controller_spec.rb +++ b/spec/controllers/projects/variables_controller_spec.rb @@ -12,7 +12,7 @@ describe Projects::VariablesController do describe 'POST #create' do context 'variable is valid' do it 'shows a success flash message' do - post :create, namespace_id: project.namespace.to_param, project_id: project.to_param, + post :create, namespace_id: project.namespace.to_param, project_id: project, variable: { key: "one", value: "two" } expect(flash[:notice]).to include 'Variables were successfully updated.' @@ -22,7 +22,7 @@ describe Projects::VariablesController do context 'variable is invalid' do it 'shows an alert flash message' do - post :create, namespace_id: project.namespace.to_param, project_id: project.to_param, + post :create, namespace_id: project.namespace.to_param, project_id: project, variable: { key: "..one", value: "two" } expect(response).to render_template("projects/variables/show") @@ -40,7 +40,7 @@ describe Projects::VariablesController do end it 'shows a success flash message' do - post :update, namespace_id: project.namespace.to_param, project_id: project.to_param, + post :update, namespace_id: project.namespace.to_param, project_id: project, id: variable.id, variable: { key: variable.key, value: 'two' } expect(flash[:notice]).to include 'Variable was successfully updated.' @@ -48,7 +48,7 @@ describe Projects::VariablesController do end it 'renders the action #show if the variable key is invalid' do - post :update, namespace_id: project.namespace.to_param, project_id: project.to_param, + post :update, namespace_id: project.namespace.to_param, project_id: project, id: variable.id, variable: { key: '?', value: variable.value } expect(response).to have_http_status(200) diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index e7aa8745b99..202759664a0 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -35,7 +35,7 @@ describe ProjectsController do let(:private_project) { create(:empty_project, :private) } it "does not initialize notification setting" do - get :show, namespace_id: private_project.namespace.path, id: private_project.path + get :show, namespace_id: private_project.namespace, id: private_project expect(assigns(:notification_setting)).to be_nil end end @@ -43,7 +43,7 @@ describe ProjectsController do context "user has access to project" do context "and does not have notification setting" do it "initializes notification as disabled" do - get :show, namespace_id: public_project.namespace.path, id: public_project.path + get :show, namespace_id: public_project.namespace, id: public_project expect(assigns(:notification_setting).level).to eq("global") end end @@ -56,7 +56,7 @@ describe ProjectsController do end it "shows current notification setting" do - get :show, namespace_id: public_project.namespace.path, id: public_project.path + get :show, namespace_id: public_project.namespace, id: public_project expect(assigns(:notification_setting).level).to eq("watch") end end @@ -71,7 +71,7 @@ describe ProjectsController do end it 'shows wiki homepage' do - get :show, namespace_id: project.namespace.path, id: project.path + get :show, namespace_id: project.namespace, id: project expect(response).to render_template('projects/_wiki') end @@ -79,7 +79,7 @@ describe ProjectsController do it 'shows issues list page if wiki is disabled' do project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED) - get :show, namespace_id: project.namespace.path, id: project.path + get :show, namespace_id: project.namespace, id: project expect(response).to render_template('projects/issues/_issues') end @@ -88,7 +88,7 @@ describe ProjectsController do project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED) project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) - get :show, namespace_id: project.namespace.path, id: project.path + get :show, namespace_id: project.namespace, id: project expect(response).to render_template("projects/_customize_workflow") end @@ -96,7 +96,7 @@ describe ProjectsController do it 'shows activity if enabled by user' do user.update_attribute(:project_view, 'activity') - get :show, namespace_id: project.namespace.path, id: project.path + get :show, namespace_id: project.namespace, id: project expect(response).to render_template("projects/_activity") end @@ -113,7 +113,7 @@ describe ProjectsController do before do user.update_attributes(project_view: project_view) - get :show, namespace_id: empty_project.namespace.path, id: empty_project.path + get :show, namespace_id: empty_project.namespace, id: empty_project end it "renders the empty project view" do @@ -133,7 +133,7 @@ describe ProjectsController do before do user.update_attributes(project_view: project_view) - get :show, namespace_id: empty_project.namespace.path, id: empty_project.path + get :show, namespace_id: empty_project.namespace, id: empty_project end it "renders the empty project view" do @@ -154,7 +154,7 @@ describe ProjectsController do allow(controller).to receive(:current_user).and_return(user) allow(user).to receive(:project_view).and_return('activity') - get :show, namespace_id: public_project.namespace.path, id: public_project.path + get :show, namespace_id: public_project.namespace, id: public_project expect(response).to render_template('_activity') end @@ -162,7 +162,7 @@ describe ProjectsController do allow(controller).to receive(:current_user).and_return(user) allow(user).to receive(:project_view).and_return('readme') - get :show, namespace_id: public_project.namespace.path, id: public_project.path + get :show, namespace_id: public_project.namespace, id: public_project expect(response).to render_template('_readme') end @@ -170,7 +170,7 @@ describe ProjectsController do allow(controller).to receive(:current_user).and_return(user) allow(user).to receive(:project_view).and_return('files') - get :show, namespace_id: public_project.namespace.path, id: public_project.path + get :show, namespace_id: public_project.namespace, id: public_project expect(response).to render_template('_files') end end @@ -178,7 +178,7 @@ describe ProjectsController do context "when requested with case sensitive namespace and project path" do context "when there is a match with the same casing" do it "loads the project" do - get :show, namespace_id: public_project.namespace.path, id: public_project.path + get :show, namespace_id: public_project.namespace, id: public_project expect(assigns(:project)).to eq(public_project) expect(response).to have_http_status(200) @@ -187,10 +187,10 @@ describe ProjectsController do context "when there is a match with different casing" do it "redirects to the normalized path" do - get :show, namespace_id: public_project.namespace.path, id: public_project.path.upcase + get :show, namespace_id: public_project.namespace, id: public_project.path.upcase expect(assigns(:project)).to eq(public_project) - expect(response).to redirect_to("/#{public_project.path_with_namespace}") + expect(response).to redirect_to("/#{public_project.full_path}") end end end @@ -208,7 +208,7 @@ describe ProjectsController do project = create(:empty_project, pending_delete: true) sign_in(user) - get :show, namespace_id: project.namespace.path, id: project.path + get :show, namespace_id: project.namespace, id: project expect(response.status).to eq 404 end @@ -218,7 +218,7 @@ describe ProjectsController do it 'redirects to project page (format.html)' do project = create(:project, :public) - get :show, namespace_id: project.namespace.path, id: project.path, format: :git + get :show, namespace_id: project.namespace, id: project, format: :git expect(response).to have_http_status(302) expect(response).to redirect_to(namespace_project_path) @@ -239,7 +239,7 @@ describe ProjectsController do sign_in(admin) put :update, - namespace_id: project.namespace.to_param, + namespace_id: project.namespace, id: project.id, project: project_params @@ -257,7 +257,7 @@ describe ProjectsController do sign_in(admin) orig_id = project.id - delete :destroy, namespace_id: project.namespace.path, id: project.path + delete :destroy, namespace_id: project.namespace, id: project expect { Project.find(orig_id) }.to raise_error(ActiveRecord::RecordNotFound) expect(response).to have_http_status(302) @@ -277,7 +277,7 @@ describe ProjectsController do project.merge_requests << merge_request sign_in(admin) - delete :destroy, namespace_id: fork_project.namespace.path, id: fork_project.path + delete :destroy, namespace_id: fork_project.namespace, id: fork_project expect(merge_request.reload.state).to eq('closed') end @@ -287,8 +287,8 @@ describe ProjectsController do describe 'PUT #new_issue_address' do subject do put :new_issue_address, - namespace_id: project.namespace.to_param, - id: project.to_param + namespace_id: project.namespace, + id: project user.reload end @@ -316,23 +316,23 @@ describe ProjectsController do sign_in(user) expect(user.starred?(public_project)).to be_falsey post(:toggle_star, - namespace_id: public_project.namespace.to_param, - id: public_project.to_param) + namespace_id: public_project.namespace, + id: public_project) expect(user.starred?(public_project)).to be_truthy post(:toggle_star, - namespace_id: public_project.namespace.to_param, - id: public_project.to_param) + namespace_id: public_project.namespace, + id: public_project) expect(user.starred?(public_project)).to be_falsey end it "does nothing if user is not signed in" do post(:toggle_star, - namespace_id: project.namespace.to_param, - id: public_project.to_param) + namespace_id: project.namespace, + id: public_project) expect(user.starred?(public_project)).to be_falsey post(:toggle_star, - namespace_id: project.namespace.to_param, - id: public_project.to_param) + namespace_id: project.namespace, + id: public_project) expect(user.starred?(public_project)).to be_falsey end end @@ -366,8 +366,8 @@ describe ProjectsController do it 'does nothing if project was not forked' do delete(:remove_fork, - namespace_id: unforked_project.namespace.to_param, - id: unforked_project.to_param, format: :js) + namespace_id: unforked_project.namespace, + id: unforked_project, format: :js) expect(flash[:notice]).to be_nil expect(response).to render_template(:remove_fork) @@ -377,8 +377,8 @@ describe ProjectsController do it "does nothing if user is not signed in" do delete(:remove_fork, - namespace_id: project.namespace.to_param, - id: project.to_param, format: :js) + namespace_id: project.namespace, + id: project, format: :js) expect(response).to have_http_status(401) end end @@ -387,7 +387,7 @@ describe ProjectsController do let(:public_project) { create(:project, :public) } it "gets a list of branches and tags" do - get :refs, namespace_id: public_project.namespace.path, id: public_project.path + get :refs, namespace_id: public_project.namespace, id: public_project parsed_body = JSON.parse(response.body) expect(parsed_body["Branches"]).to include("master") @@ -396,7 +396,7 @@ describe ProjectsController do end it "gets a list of branches, tags and commits" do - get :refs, namespace_id: public_project.namespace.path, id: public_project.path, ref: "123456" + get :refs, namespace_id: public_project.namespace, id: public_project, ref: "123456" parsed_body = JSON.parse(response.body) expect(parsed_body["Branches"]).to include("master") diff --git a/spec/controllers/root_controller_spec.rb b/spec/controllers/root_controller_spec.rb index b14d275f7fa..b32eb39b1fb 100644 --- a/spec/controllers/root_controller_spec.rb +++ b/spec/controllers/root_controller_spec.rb @@ -2,6 +2,26 @@ require 'spec_helper' describe RootController do describe 'GET index' do + context 'when user is not logged in' do + it 'redirects to the sign-in page' do + get :index + + expect(response).to redirect_to(new_user_session_path) + end + + context 'when a custom home page URL is defined' do + before do + stub_application_setting(home_page_url: 'https://gitlab.com') + end + + it 'redirects the user to the custom home page URL' do + get :index + + expect(response).to redirect_to('https://gitlab.com') + end + end + end + context 'with a user' do let(:user) { create(:user) } @@ -12,55 +32,60 @@ describe RootController do context 'who has customized their dashboard setting for starred projects' do before do - user.update_attribute(:dashboard, 'stars') + user.dashboard = 'stars' end it 'redirects to their specified dashboard' do get :index + expect(response).to redirect_to starred_dashboard_projects_path end end context 'who has customized their dashboard setting for project activities' do before do - user.update_attribute(:dashboard, 'project_activity') + user.dashboard = 'project_activity' end it 'redirects to the activity list' do get :index + expect(response).to redirect_to activity_dashboard_path end end context 'who has customized their dashboard setting for starred project activities' do before do - user.update_attribute(:dashboard, 'starred_project_activity') + user.dashboard = 'starred_project_activity' end it 'redirects to the activity list' do get :index + expect(response).to redirect_to activity_dashboard_path(filter: 'starred') end end context 'who has customized their dashboard setting for groups' do before do - user.update_attribute(:dashboard, 'groups') + user.dashboard = 'groups' end it 'redirects to their group list' do get :index + expect(response).to redirect_to dashboard_groups_path end end context 'who has customized their dashboard setting for todos' do before do - user.update_attribute(:dashboard, 'todos') + user.dashboard = 'todos' end it 'redirects to their todo list' do get :index + expect(response).to redirect_to dashboard_todos_path end end @@ -68,6 +93,7 @@ describe RootController do context 'who uses the default dashboard setting' do it 'renders the default dashboard' do get :index + expect(response).to render_template 'dashboard/projects/index' end end diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb index dadcb90cfc2..5de3b9890ef 100644 --- a/spec/controllers/snippets_controller_spec.rb +++ b/spec/controllers/snippets_controller_spec.rb @@ -139,12 +139,14 @@ describe SnippetsController do end describe 'POST #create' do - def create_snippet(snippet_params = {}) + def create_snippet(snippet_params = {}, additional_params = {}) sign_in(user) post :create, { personal_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params) - } + }.merge(additional_params) + + Snippet.last end context 'when the snippet is spam' do @@ -163,13 +165,164 @@ describe SnippetsController do it 'rejects the shippet' do expect { create_snippet(visibility_level: Snippet::PUBLIC) }. not_to change { Snippet.count } - expect(response).to render_template(:new) end it 'creates a spam log' do expect { create_snippet(visibility_level: Snippet::PUBLIC) }. to change { SpamLog.count }.by(1) end + + it 'renders :new with recaptcha disabled' do + stub_application_setting(recaptcha_enabled: false) + + create_snippet(visibility_level: Snippet::PUBLIC) + + expect(response).to render_template(:new) + end + + context 'recaptcha enabled' do + before do + stub_application_setting(recaptcha_enabled: true) + end + + it 'renders :verify with recaptcha enabled' do + create_snippet(visibility_level: Snippet::PUBLIC) + + expect(response).to render_template(:verify) + end + + it 'renders snippet page when recaptcha verified' do + spammy_title = 'Whatever' + + spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title) + snippet = create_snippet({ title: spammy_title }, + { spam_log_id: spam_logs.last.id, + recaptcha_verification: true }) + + expect(response).to redirect_to(snippet_path(snippet)) + end + end + end + end + end + + describe 'PUT #update' do + let(:project) { create :project } + let(:snippet) { create :personal_snippet, author: user, project: project, visibility_level: visibility_level } + + def update_snippet(snippet_params = {}, additional_params = {}) + sign_in(user) + + put :update, { + id: snippet.id, + personal_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params) + }.merge(additional_params) + + snippet.reload + end + + context 'when the snippet is spam' do + before do + allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) + end + + context 'when the snippet is private' do + let(:visibility_level) { Snippet::PRIVATE } + + it 'updates the snippet' do + expect { update_snippet(title: 'Foo') }. + to change { snippet.reload.title }.to('Foo') + end + end + + context 'when a private snippet is made public' do + let(:visibility_level) { Snippet::PRIVATE } + + it 'rejects the snippet' do + expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }. + not_to change { snippet.reload.title } + end + + it 'creates a spam log' do + expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }. + to change { SpamLog.count }.by(1) + end + + it 'renders :edit with recaptcha disabled' do + stub_application_setting(recaptcha_enabled: false) + + update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) + + expect(response).to render_template(:edit) + end + + context 'recaptcha enabled' do + before do + stub_application_setting(recaptcha_enabled: true) + end + + it 'renders :verify with recaptcha enabled' do + update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) + + expect(response).to render_template(:verify) + end + + it 'renders snippet page when recaptcha verified' do + spammy_title = 'Whatever' + + spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title) + snippet = update_snippet({ title: spammy_title, visibility_level: Snippet::PUBLIC }, + { spam_log_id: spam_logs.last.id, + recaptcha_verification: true }) + + expect(response).to redirect_to(snippet) + end + end + end + + context 'when the snippet is public' do + let(:visibility_level) { Snippet::PUBLIC } + + it 'rejects the shippet' do + expect { update_snippet(title: 'Foo') }. + not_to change { snippet.reload.title } + end + + it 'creates a spam log' do + expect { update_snippet(title: 'Foo') }. + to change { SpamLog.count }.by(1) + end + + it 'renders :edit with recaptcha disabled' do + stub_application_setting(recaptcha_enabled: false) + + update_snippet(title: 'Foo') + + expect(response).to render_template(:edit) + end + + context 'recaptcha enabled' do + before do + stub_application_setting(recaptcha_enabled: true) + end + + it 'renders :verify with recaptcha enabled' do + update_snippet(title: 'Foo') + + expect(response).to render_template(:verify) + end + + it 'renders snippet page when recaptcha verified' do + spammy_title = 'Whatever' + + spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title) + snippet = update_snippet({ title: spammy_title }, + { spam_log_id: spam_logs.last.id, + recaptcha_verification: true }) + + expect(response).to redirect_to(snippet_path(snippet)) + end + end end end end @@ -286,6 +439,24 @@ describe SnippetsController do expect(assigns(:snippet)).to eq(personal_snippet) expect(response).to have_http_status(200) end + + context 'CRLF line ending' do + let(:personal_snippet) do + create(:personal_snippet, :public, author: user, content: "first line\r\nsecond line\r\nthird line") + end + + it 'returns LF line endings by default' do + get action, id: personal_snippet.to_param + + expect(response.body).to eq("first line\nsecond line\nthird line") + end + + it 'does not convert line endings when parameter present' do + get action, id: personal_snippet.to_param, line_ending: :raw + + expect(response.body).to eq("first line\r\nsecond line\r\nthird line") + end + end end context 'when not signed in' do diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb index 570d9fa43f8..c9584ddf18c 100644 --- a/spec/controllers/uploads_controller_spec.rb +++ b/spec/controllers/uploads_controller_spec.rb @@ -4,6 +4,28 @@ describe UploadsController do let!(:user) { create(:user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) } describe "GET show" do + context 'Content-Disposition security measures' do + let(:project) { create(:empty_project, :public) } + + context 'for PNG files' do + it 'returns Content-Disposition: inline' do + note = create(:note, :with_attachment, project: project) + get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png' + + expect(response['Content-Disposition']).to start_with('inline;') + end + end + + context 'for SVG files' do + it 'returns Content-Disposition: attachment' do + note = create(:note, :with_svg_attachment, project: project) + get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.svg' + + expect(response['Content-Disposition']).to start_with('attachment;') + end + end + end + context "when viewing a user avatar" do context "when signed in" do before do diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 0397d5d4001..cabe128acf7 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -76,6 +76,18 @@ FactoryGirl.define do manual end + trait :tags do + tag_list [:docker, :ruby] + end + + trait :on_tag do + tag true + end + + trait :triggered do + trigger_request factory: :ci_trigger_request_with_variables + end + after(:build) do |build, evaluator| build.project = build.pipeline.project end @@ -89,8 +101,9 @@ FactoryGirl.define do tag true end - factory :ci_build_with_coverage do + trait :coverage do coverage 99.9 + coverage_regex '/(d+)/' end trait :trace do @@ -99,6 +112,16 @@ FactoryGirl.define do end end + trait :erased do + erased_at Time.now + erased_by factory: :user + end + + trait :queued do + queued_at Time.now + runner factory: :ci_runner + end + trait :artifacts do after(:create) do |build, _| build.artifacts_file = @@ -128,5 +151,17 @@ FactoryGirl.define do build.save! end end + + trait :with_commit do + after(:build) do |build| + allow(build).to receive(:commit).and_return build(:commit, :without_author) + end + end + + trait :with_commit_and_author do + after(:build) do |build| + allow(build).to receive(:commit).and_return build(:commit) + end + end end end diff --git a/spec/factories/commits.rb b/spec/factories/commits.rb index ac6eb0a7897..89e260cf65b 100644 --- a/spec/factories/commits.rb +++ b/spec/factories/commits.rb @@ -8,5 +8,15 @@ FactoryGirl.define do initialize_with do new(git_commit, project) end + + after(:build) do |commit| + allow(commit).to receive(:author).and_return build(:author) + end + + trait :without_author do + after(:build) do |commit| + allow(commit).to receive(:author).and_return nil + end + end end end diff --git a/spec/factories/keys.rb b/spec/factories/keys.rb index d69c5b38d0a..dd93b439b2b 100644 --- a/spec/factories/keys.rb +++ b/spec/factories/keys.rb @@ -2,10 +2,13 @@ FactoryGirl.define do factory :key do title key do - "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0= dummy@gitlab.com" + 'ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0= dummy@gitlab.com' end factory :deploy_key, class: 'DeployKey' do + key do + 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFf6RYK3qu/RKF/3ndJmL5xgMLp3O96x8lTay+QGZ0+9FnnAXMdUqBq/ZU6d/gyMB4IaW3nHzM1w049++yAB6UPCzMB8Uo27K5/jyZCtj7Vm9PFNjF/8am1kp46c/SeYicQgQaSBdzIW3UDEa1Ef68qroOlvpi9PYZ/tA7M0YP0K5PXX+E36zaIRnJVMPT3f2k+GnrxtjafZrwFdpOP/Fol5BQLBgcsyiU+LM1SuaCrzd8c9vyaTA1CxrkxaZh+buAi0PmdDtaDrHd42gqZkXCKavyvgM5o2CkQ5LJHCgzpXy05qNFzmThBSkb+XtoxbyagBiGbVZtSVow6Xa7qewz' + end end factory :personal_key do @@ -14,7 +17,7 @@ FactoryGirl.define do factory :another_key do key do - "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDmTillFzNTrrGgwaCKaSj+QCz81E6jBc/s9av0+3b1Hwfxgkqjl4nAK/OD2NjgyrONDTDfR8cRN4eAAy6nY8GLkOyYBDyuc5nTMqs5z3yVuTwf3koGm/YQQCmo91psZ2BgDFTor8SVEE5Mm1D1k3JDMhDFxzzrOtRYFPci9lskTJaBjpqWZ4E9rDTD2q/QZntCqbC3wE9uSemRQB5f8kik7vD/AD8VQXuzKladrZKkzkONCPWsXDspUitjM8HkQdOf0PsYn1CMUC1xKYbCxkg5TkEosIwGv6CoEArUrdu/4+10LVslq494mAvEItywzrluCLCnwELfW+h/m8UHoVhZ" + 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDmTillFzNTrrGgwaCKaSj+QCz81E6jBc/s9av0+3b1Hwfxgkqjl4nAK/OD2NjgyrONDTDfR8cRN4eAAy6nY8GLkOyYBDyuc5nTMqs5z3yVuTwf3koGm/YQQCmo91psZ2BgDFTor8SVEE5Mm1D1k3JDMhDFxzzrOtRYFPci9lskTJaBjpqWZ4E9rDTD2q/QZntCqbC3wE9uSemRQB5f8kik7vD/AD8VQXuzKladrZKkzkONCPWsXDspUitjM8HkQdOf0PsYn1CMUC1xKYbCxkg5TkEosIwGv6CoEArUrdu/4+10LVslq494mAvEItywzrluCLCnwELfW+h/m8UHoVhZ' end factory :another_deploy_key, class: 'DeployKey' do diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index 22f84150bb3..ae0bbbd6aeb 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -59,8 +59,8 @@ FactoryGirl.define do target_branch "master" end - trait :merge_when_build_succeeds do - merge_when_build_succeeds true + trait :merge_when_pipeline_succeeds do + merge_when_pipeline_succeeds true merge_user author end diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index a21da7074f9..5c50cd7f4ad 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -97,7 +97,11 @@ FactoryGirl.define do end trait :with_attachment do - attachment { fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "`/png") } + attachment { fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png") } + end + + trait :with_svg_attachment do + attachment { fixture_file_upload(Rails.root + "spec/fixtures/unsanitized.svg", "image/svg+xml") } end end end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index c80b09e9b9d..04de3512125 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -39,6 +39,10 @@ FactoryGirl.define do trait :empty_repo do after(:create) do |project| project.create_repository + + # We delete hooks so that gitlab-shell will not try to authenticate with + # an API that isn't running + FileUtils.rm_r(File.join(project.repository_storage_path, "#{project.path_with_namespace}.git", 'hooks')) end end @@ -138,27 +142,24 @@ FactoryGirl.define do project.add_user(args[:user], args[:access]) - project.repository.commit_file( + project.repository.create_file( args[:user], ".gitlab/#{args[:path]}/bug.md", 'something valid', message: 'test 3', - branch_name: 'master', - update: false) - project.repository.commit_file( + branch_name: 'master') + project.repository.create_file( args[:user], ".gitlab/#{args[:path]}/template_test.md", 'template_test', message: 'test 1', - branch_name: 'master', - update: false) - project.repository.commit_file( + branch_name: 'master') + project.repository.create_file( args[:user], ".gitlab/#{args[:path]}/feature_proposal.md", 'feature_proposal', message: 'test 2', - branch_name: 'master', - update: false) + branch_name: 'master') end end end diff --git a/spec/factories/services.rb b/spec/factories/services.rb index a14a46c803e..51335bdcf1d 100644 --- a/spec/factories/services.rb +++ b/spec/factories/services.rb @@ -2,4 +2,14 @@ FactoryGirl.define do factory :service do project factory: :empty_project end + + factory :kubernetes_service do + project factory: :empty_project + active true + properties({ + namespace: 'somepath', + api_url: 'https://kubernetes.example.com', + token: 'a' * 40, + }) + end end diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb index 275561502cd..c1ac3bb84ad 100644 --- a/spec/factories/todos.rb +++ b/spec/factories/todos.rb @@ -14,9 +14,8 @@ FactoryGirl.define do action { Todo::MENTIONED } end - trait :on_commit do - commit_id RepoHelpers.sample_commit.id - target_type "Commit" + trait :directly_addressed do + action { Todo::DIRECTLY_ADDRESSED } end trait :build_failed do @@ -36,8 +35,21 @@ FactoryGirl.define do action { Todo::UNMERGEABLE } end + trait :pending do + state :pending + end + trait :done do state :done end end + + factory :on_commit_todo, class: Todo do + project factory: :empty_project + author + user + action { Todo::ASSIGNED } + commit_id RepoHelpers.sample_commit.id + target_type "Commit" + end end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index c6f7869516e..249dabbaae8 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -14,10 +14,23 @@ FactoryGirl.define do admin true end + trait :blocked do + after(:build) { |user, _| user.block! } + end + + trait :external do + external true + end + trait :two_factor do two_factor_via_otp end + trait :ghost do + ghost true + after(:build) { |user, _| user.block! } + end + trait :two_factor_via_otp do before(:create) do |user| user.otp_required_for_login = true diff --git a/spec/factories/wiki_directories.rb b/spec/factories/wiki_directories.rb new file mode 100644 index 00000000000..3f3c864ac2b --- /dev/null +++ b/spec/factories/wiki_directories.rb @@ -0,0 +1,6 @@ +FactoryGirl.define do + factory :wiki_directory do + slug '/path_up_to/dir' + initialize_with { new(slug) } + end +end diff --git a/spec/factories/wiki_pages.rb b/spec/factories/wiki_pages.rb index efa6cbe5bb1..4105f59e289 100644 --- a/spec/factories/wiki_pages.rb +++ b/spec/factories/wiki_pages.rb @@ -2,8 +2,26 @@ require 'ostruct' FactoryGirl.define do factory :wiki_page do + transient do + attrs do + { + title: 'Title', + content: 'Content for wiki page', + format: 'markdown' + } + end + end + page { OpenStruct.new(url_path: 'some-name') } association :wiki, factory: :project_wiki, strategy: :build initialize_with { new(wiki, page, true) } + + before(:create) do |page, evaluator| + page.attributes = evaluator.attrs + end + + to_create do |page| + page.create + end end end diff --git a/spec/features/admin/admin_abuse_reports_spec.rb b/spec/features/admin/admin_abuse_reports_spec.rb index 7fcfe5a54c7..340884fc986 100644 --- a/spec/features/admin/admin_abuse_reports_spec.rb +++ b/spec/features/admin/admin_abuse_reports_spec.rb @@ -30,5 +30,24 @@ describe "Admin::AbuseReports", feature: true, js: true do end end end + + describe 'if a many users have been reported for abuse' do + let(:report_count) { AbuseReport.default_per_page + 3 } + + before do + report_count.times do + create(:abuse_report, user: create(:user)) + end + end + + describe 'in the abuse report view' do + it 'presents information about abuse report' do + visit admin_abuse_reports_path + + expect(page).to have_selector('.pagination') + expect(page).to have_selector('.pagination .page', count: (report_count.to_f / AbuseReport.default_per_page).ceil) + end + end + end end end diff --git a/spec/features/admin/admin_disables_git_access_protocol_spec.rb b/spec/features/admin/admin_disables_git_access_protocol_spec.rb index e8e080ce3e2..273cacd82cd 100644 --- a/spec/features/admin/admin_disables_git_access_protocol_spec.rb +++ b/spec/features/admin/admin_disables_git_access_protocol_spec.rb @@ -32,7 +32,7 @@ feature 'Admin disables Git access protocol', feature: true do scenario 'shows only HTTP url' do visit_project - expect(page).to have_content("git clone #{project.http_url_to_repo}") + expect(page).to have_content("git clone #{project.http_url_to_repo(admin)}") expect(page).not_to have_selector('#clone-dropdown') end end diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb index f05fbe3d062..5dcc7d35d82 100644 --- a/spec/features/admin/admin_runners_spec.rb +++ b/spec/features/admin/admin_runners_spec.rb @@ -18,7 +18,7 @@ describe "Admin Runners" do it 'has all necessary texts' do expect(page).to have_text "To register a new Runner" - expect(page).to have_text "Runners with last contact less than a minute ago: 1" + expect(page).to have_text "Runners with last contact more than a minute ago: 1" end describe 'search' do diff --git a/spec/features/atom/dashboard_issues_spec.rb b/spec/features/atom/dashboard_issues_spec.rb index 21ee6cedbae..a7c22615b89 100644 --- a/spec/features/atom/dashboard_issues_spec.rb +++ b/spec/features/atom/dashboard_issues_spec.rb @@ -23,7 +23,7 @@ describe "Dashboard Issues Feed", feature: true do visit issues_dashboard_path(:atom, private_token: user.private_token, state: 'opened', assignee_id: user.id) link = find('link[type="application/atom+xml"]') - params = CGI::parse(URI.parse(link[:href]).query) + params = CGI.parse(URI.parse(link[:href]).query) expect(params).to include('private_token' => [user.private_token]) expect(params).to include('state' => ['opened']) diff --git a/spec/features/atom/issues_spec.rb b/spec/features/atom/issues_spec.rb index 863412d18eb..a01a050a013 100644 --- a/spec/features/atom/issues_spec.rb +++ b/spec/features/atom/issues_spec.rb @@ -43,7 +43,7 @@ describe 'Issues Feed', feature: true do :atom, private_token: user.private_token, state: 'opened', assignee_id: user.id) link = find('link[type="application/atom+xml"]') - params = CGI::parse(URI.parse(link[:href]).query) + params = CGI.parse(URI.parse(link[:href]).query) expect(params).to include('private_token' => [user.private_token]) expect(params).to include('state' => ['opened']) @@ -54,7 +54,7 @@ describe 'Issues Feed', feature: true do visit issues_group_path(group, :atom, private_token: user.private_token, state: 'opened', assignee_id: user.id) link = find('link[type="application/atom+xml"]') - params = CGI::parse(URI.parse(link[:href]).query) + params = CGI.parse(URI.parse(link[:href]).query) expect(params).to include('private_token' => [user.private_token]) expect(params).to include('state' => ['opened']) diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb index f8c3ccb416b..b740e191f48 100644 --- a/spec/features/atom/users_spec.rb +++ b/spec/features/atom/users_spec.rb @@ -61,7 +61,7 @@ describe "User Feed", feature: true do end it 'has XHTML summaries in merge request descriptions' do - expect(body).to match /Here is the fix: <\/p><div[^>]*><a[^>]*><img[^>]*\/><\/a><\/div>/ + expect(body).to match /Here is the fix: <a[^>]*><img[^>]*\/><\/a>/ end end end diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb index 2875fc1e533..a3e24bb5ffa 100644 --- a/spec/features/boards/add_issues_modal_spec.rb +++ b/spec/features/boards/add_issues_modal_spec.rb @@ -49,6 +49,12 @@ describe 'Issue Boards add issue modal', :feature, :js do expect(page).not_to have_selector('.add-issues-modal') end + + it 'does not show tooltip on add issues button' do + button = page.find('.issue-boards-search button', text: 'Add issues') + + expect(button[:title]).not_to eq("Please add a list to your board first") + end end context 'issues list' do diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index 7225f38b7e5..e247bfa2980 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -28,6 +28,12 @@ describe 'Issue Boards', feature: true, js: true do expect(page).to have_content('Welcome to your Issue Board!') end + it 'shows tooltip on add issues button' do + button = page.find('.issue-boards-search button', text: 'Add issues') + + expect(button[:"data-original-title"]).to eq("Please add a list to your board first") + end + it 'hides the blank state when clicking nevermind button' do page.within(find('.board-blank-state')) do click_button("Nevermind, I'll use my own") diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index bad6b56a18a..59e87b3f69c 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -15,8 +15,11 @@ describe 'Issue Boards', feature: true, js: true do let!(:issue2) { create(:labeled_issue, project: project, labels: [development, stretch]) } let(:board) { create(:board, project: project) } let!(:list) { create(:list, board: board, label: development, position: 0) } + let(:card) { first('.board').first('.card') } before do + Timecop.freeze + project.team << [user, :master] login_as(user) @@ -25,44 +28,38 @@ describe 'Issue Boards', feature: true, js: true do wait_for_vue_resource end + after do + Timecop.return + end + it 'shows sidebar when clicking issue' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) expect(page).to have_selector('.issue-boards-sidebar') end it 'closes sidebar when clicking issue' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) expect(page).to have_selector('.issue-boards-sidebar') - page.within(first('.board')) do - first('.card').click - end + click_card(card) expect(page).not_to have_selector('.issue-boards-sidebar') end it 'closes sidebar when clicking close button' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) expect(page).to have_selector('.issue-boards-sidebar') - find('.gutter-toggle').click + find('.gutter-toggle').trigger('click') expect(page).not_to have_selector('.issue-boards-sidebar') end it 'shows issue details when sidebar is open' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) page.within('.issue-boards-sidebar') do expect(page).to have_content(issue2.title) @@ -70,15 +67,15 @@ describe 'Issue Boards', feature: true, js: true do end end - it 'removes card from board when clicking remove button' do - page.within(first('.board')) do - first('.card').click - end + it 'removes card from board when clicking ' do + click_card(card) page.within('.issue-boards-sidebar') do click_button 'Remove from board' end + wait_for_vue_resource + page.within(first('.board')) do expect(page).to have_selector('.card', count: 1) end @@ -86,9 +83,7 @@ describe 'Issue Boards', feature: true, js: true do context 'assignee' do it 'updates the issues assignee' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) page.within('.assignee') do click_link 'Edit' @@ -104,17 +99,12 @@ describe 'Issue Boards', feature: true, js: true do expect(page).to have_content(user.name) end - page.within(first('.board')) do - page.within(first('.card')) do - expect(page).to have_selector('.avatar') - end - end + expect(card).to have_selector('.avatar') end it 'removes the assignee' do - page.within(first('.board')) do - find('.card:nth-child(2)').click - end + card_two = first('.board').find('.card:nth-child(2)') + click_card(card_two) page.within('.assignee') do click_link 'Edit' @@ -130,17 +120,11 @@ describe 'Issue Boards', feature: true, js: true do expect(page).to have_content('No assignee') end - page.within(first('.board')) do - page.within(find('.card:nth-child(2)')) do - expect(page).not_to have_selector('.avatar') - end - end + expect(card_two).not_to have_selector('.avatar') end it 'assignees to current user' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) page.within(find('.assignee')) do expect(page).to have_content('No assignee') @@ -152,17 +136,11 @@ describe 'Issue Boards', feature: true, js: true do expect(page).to have_content(user.name) end - page.within(first('.board')) do - page.within(first('.card')) do - expect(page).to have_selector('.avatar') - end - end + expect(card).to have_selector('.avatar') end it 'resets assignee dropdown' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) page.within('.assignee') do click_link 'Edit' @@ -192,9 +170,7 @@ describe 'Issue Boards', feature: true, js: true do context 'milestone' do it 'adds a milestone' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) page.within('.milestone') do click_link 'Edit' @@ -212,9 +188,7 @@ describe 'Issue Boards', feature: true, js: true do end it 'removes a milestone' do - page.within(first('.board')) do - find('.card:nth-child(2)').click - end + click_card(card) page.within('.milestone') do click_link 'Edit' @@ -234,9 +208,7 @@ describe 'Issue Boards', feature: true, js: true do context 'due date' do it 'updates due date' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) page.within('.due_date') do click_link 'Edit' @@ -252,9 +224,7 @@ describe 'Issue Boards', feature: true, js: true do context 'labels' do it 'adds a single label' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) page.within('.labels') do click_link 'Edit' @@ -273,18 +243,12 @@ describe 'Issue Boards', feature: true, js: true do end end - page.within(first('.board')) do - page.within(first('.card')) do - expect(page).to have_selector('.label', count: 2) - expect(page).to have_content(bug.title) - end - end + expect(card).to have_selector('.label', count: 2) + expect(card).to have_content(bug.title) end it 'adds a multiple labels' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) page.within('.labels') do click_link 'Edit' @@ -305,19 +269,13 @@ describe 'Issue Boards', feature: true, js: true do end end - page.within(first('.board')) do - page.within(first('.card')) do - expect(page).to have_selector('.label', count: 3) - expect(page).to have_content(bug.title) - expect(page).to have_content(regression.title) - end - end + expect(card).to have_selector('.label', count: 3) + expect(card).to have_content(bug.title) + expect(card).to have_content(regression.title) end it 'removes a label' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) page.within('.labels') do click_link 'Edit' @@ -336,20 +294,14 @@ describe 'Issue Boards', feature: true, js: true do end end - page.within(first('.board')) do - page.within(first('.card')) do - expect(page).not_to have_selector('.label') - expect(page).not_to have_content(stretch.title) - end - end + expect(card).not_to have_selector('.label') + expect(card).not_to have_content(stretch.title) end end context 'subscription' do it 'changes issue subscription' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) page.within('.subscription') do click_button 'Subscribe' @@ -358,4 +310,19 @@ describe 'Issue Boards', feature: true, js: true do end end end + + def click_card(card) + page.within(card) do + first('.card-number').click + end + + wait_for_sidebar + end + + def wait_for_sidebar + # loop until the CSS transition is complete + Timeout.timeout(0.5) do + loop until evaluate_script('$(".right-sidebar").outerWidth()') == 290 + end + end end diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb index 3e0b6364e0d..35d090c4b7f 100644 --- a/spec/features/calendar_spec.rb +++ b/spec/features/calendar_spec.rb @@ -1,9 +1,11 @@ require 'spec_helper' -feature 'Contributions Calendar', js: true, feature: true do +feature 'Contributions Calendar', :feature, :js do include WaitForAjax + let(:user) { create(:user) } let(:contributed_project) { create(:project, :public) } + let(:issue_note) { create(:note, project: contributed_project) } # Ex/ Sunday Jan 1, 2016 date_format = '%A %b %-d, %Y' @@ -12,31 +14,31 @@ feature 'Contributions Calendar', js: true, feature: true do issue_params = { title: issue_title } def get_cell_color_selector(contributions) - contribution_cell = '.user-contrib-cell' - activity_colors = Array['#ededed', '#acd5f2', '#7fa8c9', '#527ba0', '#254e77'] - activity_colors_index = 0 - - if contributions > 0 && contributions < 10 - activity_colors_index = 1 - elsif contributions >= 10 && contributions < 20 - activity_colors_index = 2 - elsif contributions >= 20 && contributions < 30 - activity_colors_index = 3 - elsif contributions >= 30 - activity_colors_index = 4 - end + activity_colors = %w[#ededed #acd5f2 #7fa8c9 #527ba0 #254e77] + # We currently don't actually test the cases with contributions >= 20 + activity_colors_index = + if contributions > 0 && contributions < 10 + 1 + elsif contributions >= 10 && contributions < 20 + 2 + elsif contributions >= 20 && contributions < 30 + 3 + elsif contributions >= 30 + 4 + else + 0 + end - "#{contribution_cell}[fill='#{activity_colors[activity_colors_index]}']" + ".user-contrib-cell[fill='#{activity_colors[activity_colors_index]}']" end def get_cell_date_selector(contributions, date) - contribution_text = 'No contributions' - - if contributions === 1 - contribution_text = '1 contribution' - elsif contributions > 1 - contribution_text = "#{contributions} contributions" - end + contribution_text = + if contributions.zero? + 'No contributions' + else + "#{contributions} #{'contribution'.pluralize(contributions)}" + end "#{get_cell_color_selector(contributions)}[data-original-title='#{contribution_text}<br />#{date}']" end @@ -45,129 +47,155 @@ feature 'Contributions Calendar', js: true, feature: true do push_params = { project: contributed_project, action: Event::PUSHED, - author_id: @user.id, + author_id: user.id, data: { commit_count: 3 } } Event.create(push_params) end - def get_first_cell_content - find('.user-calendar-activities').text - end + def note_comment_contribution + note_comment_params = { + project: contributed_project, + action: Event::COMMENTED, + target: issue_note, + author_id: user.id + } - before do - login_as :user - visit @user.username - wait_for_ajax + Event.create(note_comment_params) end - it 'displays calendar', js: true do - expect(page).to have_css('.js-contrib-calendar') + def selected_day_activities + find('.user-calendar-activities').text end - describe 'select calendar day', js: true do - let(:cells) { page.all('.user-contrib-cell') } - let(:first_cell_content_before) { get_first_cell_content } + before do + login_as user + end + describe 'calendar day selection' do before do - cells[0].click + visit user.username wait_for_ajax - first_cell_content_before end - it 'displays calendar day activities', js: true do - expect(get_first_cell_content).not_to eq('') + it 'displays calendar' do + expect(page).to have_css('.js-contrib-calendar') end - describe 'select another calendar day', js: true do + describe 'select calendar day' do + let(:cells) { page.all('.user-contrib-cell') } + before do - cells[1].click + cells[0].click wait_for_ajax + @first_day_activities = selected_day_activities end - it 'displays different calendar day activities', js: true do - expect(get_first_cell_content).not_to eq(first_cell_content_before) + it 'displays calendar day activities' do + expect(selected_day_activities).not_to be_empty end - end - describe 'deselect calendar day', js: true do - before do - cells[0].click - wait_for_ajax + describe 'select another calendar day' do + before do + cells[1].click + wait_for_ajax + end + + it 'displays different calendar day activities' do + expect(selected_day_activities).not_to eq(@first_day_activities) + end end - it 'hides calendar day activities', js: true do - expect(get_first_cell_content).to eq('') + describe 'deselect calendar day' do + before do + cells[0].click + wait_for_ajax + end + + it 'hides calendar day activities' do + expect(selected_day_activities).to be_empty + end end end end - describe '1 calendar activity' do - before do - Issues::CreateService.new(contributed_project, @user, issue_params).execute - visit @user.username - wait_for_ajax + describe 'calendar daily activities' do + shared_context 'visit user page' do + before do + visit user.username + wait_for_ajax + end end - it 'displays calendar activity log', js: true do - expect(find('.content_list .event-note')).to have_content issue_title - end + shared_examples 'a day with activity' do |contribution_count:| + include_context 'visit user page' - it 'displays calendar activity square color for 1 contribution', js: true do - expect(page).to have_selector(get_cell_color_selector(1), count: 1) - end + it 'displays calendar activity square color for 1 contribution' do + expect(page).to have_selector(get_cell_color_selector(contribution_count), count: 1) + end - it 'displays calendar activity square on the correct date', js: true do - today = Date.today.strftime(date_format) - expect(page).to have_selector(get_cell_date_selector(1, today), count: 1) + it 'displays calendar activity square on the correct date' do + today = Date.today.strftime(date_format) + expect(page).to have_selector(get_cell_date_selector(contribution_count, today), count: 1) + end end - end - describe '10 calendar activities' do - before do - (0..9).each do |i| - push_code_contribution() + describe '1 issue creation calendar activity' do + before do + Issues::CreateService.new(contributed_project, user, issue_params).execute end - visit @user.username - wait_for_ajax - end + it_behaves_like 'a day with activity', contribution_count: 1 - it 'displays calendar activity square color for 10 contributions', js: true do - expect(page).to have_selector(get_cell_color_selector(10), count: 1) - end + describe 'issue title is shown on activity page' do + include_context 'visit user page' - it 'displays calendar activity square on the correct date', js: true do - today = Date.today.strftime(date_format) - expect(page).to have_selector(get_cell_date_selector(10, today), count: 1) + it 'displays calendar activity log' do + expect(find('.content_list .event-note')).to have_content issue_title + end + end end - end - describe 'calendar activity on two days' do - before do - push_code_contribution() - - Timecop.freeze(Date.yesterday) - Issues::CreateService.new(contributed_project, @user, issue_params).execute - Timecop.return + describe '1 comment calendar activity' do + before do + note_comment_contribution + end - visit @user.username - wait_for_ajax + it_behaves_like 'a day with activity', contribution_count: 1 end - it 'displays calendar activity squares for both days', js: true do - expect(page).to have_selector(get_cell_color_selector(1), count: 2) - end + describe '10 calendar activities' do + before do + 10.times { push_code_contribution } + end - it 'displays calendar activity square for yesterday', js: true do - yesterday = Date.yesterday.strftime(date_format) - expect(page).to have_selector(get_cell_date_selector(1, yesterday), count: 1) + it_behaves_like 'a day with activity', contribution_count: 10 end - it 'displays calendar activity square for today', js: true do - today = Date.today.strftime(date_format) - expect(page).to have_selector(get_cell_date_selector(1, today), count: 1) + describe 'calendar activity on two days' do + before do + push_code_contribution + + Timecop.freeze(Date.yesterday) do + Issues::CreateService.new(contributed_project, user, issue_params).execute + end + end + include_context 'visit user page' + + it 'displays calendar activity squares for both days' do + expect(page).to have_selector(get_cell_color_selector(1), count: 2) + end + + it 'displays calendar activity square for yesterday' do + yesterday = Date.yesterday.strftime(date_format) + expect(page).to have_selector(get_cell_date_selector(1, yesterday), count: 1) + end + + it 'displays calendar activity square for today' do + today = Date.today.strftime(date_format) + expect(page).to have_selector(get_cell_date_selector(1, today), count: 1) + end end end end diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index dede562c2b0..0e305c52358 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -153,7 +153,7 @@ describe 'Commits' do expect(page).to have_content pipeline.git_author_name expect(page).to have_link('Download artifacts') expect(page).not_to have_link('Cancel running') - expect(page).not_to have_link('Retry failed') + expect(page).not_to have_link('Retry') end end @@ -172,7 +172,7 @@ describe 'Commits' do expect(page).to have_content pipeline.git_author_name expect(page).not_to have_link('Download artifacts') expect(page).not_to have_link('Cancel running') - expect(page).not_to have_link('Retry failed') + expect(page).not_to have_link('Retry') end end end diff --git a/spec/features/dashboard/active_tab_spec.rb b/spec/features/dashboard/active_tab_spec.rb index 7d59fcac517..ae750be4d4a 100644 --- a/spec/features/dashboard/active_tab_spec.rb +++ b/spec/features/dashboard/active_tab_spec.rb @@ -1,14 +1,15 @@ require 'spec_helper' -RSpec.describe 'Dashboard Active Tab', feature: true do +RSpec.describe 'Dashboard Active Tab', js: true, feature: true do before do login_as :user end shared_examples 'page has active tab' do |title| it "#{title} tab" do - expect(page).to have_selector('.nav-sidebar li.active', count: 1) - expect(find('.nav-sidebar li.active')).to have_content(title) + find('.global-dropdown-toggle').trigger('click') + expect(page).to have_selector('.global-dropdown-menu li.active', count: 1) + expect(find('.global-dropdown-menu li.active')).to have_content(title) end end diff --git a/spec/features/dashboard/activity_spec.rb b/spec/features/dashboard/activity_spec.rb new file mode 100644 index 00000000000..c977f266296 --- /dev/null +++ b/spec/features/dashboard/activity_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +RSpec.describe 'Dashboard Activity', feature: true do + before do + login_as(create :user) + visit activity_dashboard_path + end + + it_behaves_like "it has an RSS button with current_user's private token" + it_behaves_like "an autodiscoverable RSS feed with current_user's private token" +end diff --git a/spec/features/dashboard/archived_projects_spec.rb b/spec/features/dashboard/archived_projects_spec.rb index 038c1641be9..f33bcbb5318 100644 --- a/spec/features/dashboard/archived_projects_spec.rb +++ b/spec/features/dashboard/archived_projects_spec.rb @@ -25,4 +25,19 @@ RSpec.describe 'Dashboard Archived Project', feature: true do expect(page).to have_link(project.name) expect(page).to have_link(archived_project.name) end + + it 'searchs archived projects', :js do + click_button 'Last updated' + click_link 'Show archived projects' + + expect(page).to have_link(project.name) + expect(page).to have_link(archived_project.name) + + fill_in 'project-filter-form-field', with: archived_project.name + + find('#project-filter-form-field').native.send_keys :return + + expect(page).not_to have_link(project.name) + expect(page).to have_link(archived_project.name) + end end diff --git a/spec/features/dashboard/groups_list_spec.rb b/spec/features/dashboard/groups_list_spec.rb new file mode 100644 index 00000000000..ca04107d33a --- /dev/null +++ b/spec/features/dashboard/groups_list_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe 'Dashboard Groups page', js: true, feature: true do + include WaitForAjax + + let!(:user) { create :user } + let!(:group) { create(:group) } + let!(:nested_group) { create(:group, :nested) } + let!(:another_group) { create(:group) } + + before do + group.add_owner(user) + nested_group.add_owner(user) + + login_as(user) + + visit dashboard_groups_path + end + + it 'shows groups user is member of' do + expect(page).to have_content(group.full_name) + expect(page).to have_content(nested_group.full_name) + expect(page).not_to have_content(another_group.full_name) + end + + it 'filters groups' do + fill_in 'filter_groups', with: group.name + wait_for_ajax + + expect(page).to have_content(group.full_name) + expect(page).not_to have_content(nested_group.full_name) + expect(page).not_to have_content(another_group.full_name) + end + + it 'resets search when user cleans the input' do + fill_in 'filter_groups', with: group.name + wait_for_ajax + + fill_in 'filter_groups', with: "" + wait_for_ajax + + expect(page).to have_content(group.full_name) + expect(page).to have_content(nested_group.full_name) + expect(page).not_to have_content(another_group.full_name) + expect(page.all('.js-groups-list-holder .content-list li').length).to eq 2 + end +end diff --git a/spec/features/dashboard/issuables_counter_spec.rb b/spec/features/dashboard/issuables_counter_spec.rb index 41dcfe439c2..a1718912fc6 100644 --- a/spec/features/dashboard/issuables_counter_spec.rb +++ b/spec/features/dashboard/issuables_counter_spec.rb @@ -35,8 +35,9 @@ describe 'Navigation bar counter', feature: true, js: true, caching: true do end def expect_counters(issuable_type, count) - dashboard_count = find('li.active span.badge') - nav_count = find(".dashboard-shortcuts-#{issuable_type} span.count") + dashboard_count = find('li.active') + find('.global-dropdown-toggle').click + nav_count = find(".dashboard-shortcuts-#{issuable_type}") expect(nav_count).to have_content(count) expect(dashboard_count).to have_content(count) diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb new file mode 100644 index 00000000000..f4420814c3a --- /dev/null +++ b/spec/features/dashboard/issues_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +RSpec.describe 'Dashboard Issues', feature: true do + let(:current_user) { create :user } + let(:public_project) { create(:empty_project, :public) } + let(:project) do + create(:empty_project) do |project| + project.team << [current_user, :master] + end + end + + let!(:authored_issue) { create :issue, author: current_user, project: project } + let!(:authored_issue_on_public_project) { create :issue, author: current_user, project: public_project } + let!(:assigned_issue) { create :issue, assignee: current_user, project: project } + let!(:other_issue) { create :issue, project: project } + + before do + login_as(current_user) + + visit issues_dashboard_path(assignee_id: current_user.id) + end + + it 'shows issues assigned to current user' do + expect(page).to have_content(assigned_issue.title) + expect(page).not_to have_content(authored_issue.title) + expect(page).not_to have_content(other_issue.title) + end + + it 'shows issues when current user is author', js: true do + find('#assignee_id', visible: false).set('') + find('.js-author-search', match: :first).click + find('.dropdown-menu-author li a', match: :first, text: current_user.to_reference).click + + expect(page).to have_content(authored_issue.title) + expect(page).to have_content(authored_issue_on_public_project.title) + expect(page).not_to have_content(assigned_issue.title) + expect(page).not_to have_content(other_issue.title) + end + + it 'shows all issues' do + click_link('Reset filters') + + expect(page).to have_content(authored_issue.title) + expect(page).to have_content(authored_issue_on_public_project.title) + expect(page).to have_content(assigned_issue.title) + expect(page).to have_content(other_issue.title) + end + + it_behaves_like "it has an RSS button with current_user's private token" + it_behaves_like "an autodiscoverable RSS feed with current_user's private token" +end diff --git a/spec/features/dashboard/project_member_activity_index_spec.rb b/spec/features/dashboard/project_member_activity_index_spec.rb index ba77093a6d4..49d93db58a9 100644 --- a/spec/features/dashboard/project_member_activity_index_spec.rb +++ b/spec/features/dashboard/project_member_activity_index_spec.rb @@ -12,7 +12,7 @@ feature 'Project member activity', feature: true, js: true do def visit_activities_and_wait_with_event(event_type) Event.create(project: project, author_id: user.id, action: event_type) - visit activity_namespace_project_path(project.namespace.path, project.path) + visit activity_namespace_project_path(project.namespace, project) wait_for_ajax end diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb new file mode 100644 index 00000000000..63eb5c697c2 --- /dev/null +++ b/spec/features/dashboard/projects_spec.rb @@ -0,0 +1,10 @@ +require 'spec_helper' + +RSpec.describe 'Dashboard Projects', feature: true do + before do + login_as(create :user) + visit dashboard_projects_path + end + + it_behaves_like "an autodiscoverable RSS feed with current_user's private token" +end diff --git a/spec/features/dashboard/shortcuts_spec.rb b/spec/features/dashboard/shortcuts_spec.rb index d9be4e5dbdd..62a2c54c94c 100644 --- a/spec/features/dashboard/shortcuts_spec.rb +++ b/spec/features/dashboard/shortcuts_spec.rb @@ -10,20 +10,20 @@ feature 'Dashboard shortcuts', feature: true, js: true do find('body').native.send_key('g') find('body').native.send_key('p') - ensure_active_main_tab('Projects') + check_page_title('Projects') find('body').native.send_key('g') find('body').native.send_key('i') - ensure_active_main_tab('Issues') + check_page_title('Issues') find('body').native.send_key('g') find('body').native.send_key('m') - ensure_active_main_tab('Merge Requests') + check_page_title('Merge Requests') end - def ensure_active_main_tab(content) - expect(find('.nav-sidebar li.active')).to have_content(content) + def check_page_title(title) + expect(find('.header-content .title')).to have_content(title) end end diff --git a/spec/features/dashboard_issues_spec.rb b/spec/features/dashboard_issues_spec.rb index b898f9bc64f..aa75e1140f6 100644 --- a/spec/features/dashboard_issues_spec.rb +++ b/spec/features/dashboard_issues_spec.rb @@ -49,9 +49,9 @@ describe "Dashboard Issues filtering", feature: true, js: true do visit_issues(milestone_title: '', assignee_id: user.id) link = find('.nav-controls a', text: 'Subscribe') - params = CGI::parse(URI.parse(link[:href]).query) + params = CGI.parse(URI.parse(link[:href]).query) auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) - auto_discovery_params = CGI::parse(URI.parse(auto_discovery_link[:href]).query) + auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query) expect(params).to include('private_token' => [user.private_token]) expect(params).to include('milestone_title' => ['']) diff --git a/spec/features/environment_spec.rb b/spec/features/environment_spec.rb index 2f49e89b4e4..c203e1f20c1 100644 --- a/spec/features/environment_spec.rb +++ b/spec/features/environment_spec.rb @@ -101,6 +101,22 @@ feature 'Environment', :feature do scenario 'it shows the terminal button' do expect(page).to have_terminal_button end + + context 'web terminal', :js do + before do + # Stub #terminals as it causes js-enabled feature specs to render the page incorrectly + allow_any_instance_of(Environment).to receive(:terminals) { nil } + visit terminal_namespace_project_environment_path(project.namespace, project, environment) + end + + it 'displays a web terminal' do + expect(page).to have_selector('#terminal') + end + + it 'displays a link to the environment external url' do + expect(page).to have_link(nil, href: environment.external_url) + end + end end context 'for developer' do diff --git a/spec/features/explore/groups_list_spec.rb b/spec/features/explore/groups_list_spec.rb new file mode 100644 index 00000000000..773ae4b38bc --- /dev/null +++ b/spec/features/explore/groups_list_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +describe 'Explore Groups page', js: true, feature: true do + include WaitForAjax + + let!(:user) { create :user } + let!(:group) { create(:group) } + let!(:public_group) { create(:group, :public) } + let!(:private_group) { create(:group, :private) } + + before do + group.add_owner(user) + + login_as(user) + + visit explore_groups_path + end + + it 'shows groups user is member of' do + expect(page).to have_content(group.full_name) + expect(page).to have_content(public_group.full_name) + expect(page).not_to have_content(private_group.full_name) + end + + it 'filters groups' do + fill_in 'filter_groups', with: group.name + wait_for_ajax + + expect(page).to have_content(group.full_name) + expect(page).not_to have_content(public_group.full_name) + expect(page).not_to have_content(private_group.full_name) + end + + it 'resets search when user cleans the input' do + fill_in 'filter_groups', with: group.name + wait_for_ajax + + fill_in 'filter_groups', with: "" + wait_for_ajax + + expect(page).to have_content(group.full_name) + expect(page).to have_content(public_group.full_name) + expect(page).not_to have_content(private_group.full_name) + expect(page.all('.js-groups-list-holder .content-list li').length).to eq 2 + end +end diff --git a/spec/features/groups/activity_spec.rb b/spec/features/groups/activity_spec.rb new file mode 100644 index 00000000000..3b481cba424 --- /dev/null +++ b/spec/features/groups/activity_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +feature 'Group activity page', feature: true do + let(:group) { create(:group) } + let(:path) { activity_group_path(group) } + + context 'when signed in' do + before do + user = create(:group_member, :developer, user: create(:user), group: group ).user + login_as(user) + visit path + end + + it_behaves_like "it has an RSS button with current_user's private token" + it_behaves_like "an autodiscoverable RSS feed with current_user's private token" + end + + context 'when signed out' do + before do + visit path + end + + it_behaves_like "it has an RSS button without a private token" + it_behaves_like "an autodiscoverable RSS feed without a private token" + end +end diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb index 476eca17a9d..1b3747c390b 100644 --- a/spec/features/groups/issues_spec.rb +++ b/spec/features/groups/issues_spec.rb @@ -5,4 +5,22 @@ feature 'Group issues page', feature: true do let(:issuable) { create(:issue, project: project, title: "this is my created issuable")} include_examples 'project features apply to issuables', Issue + + context 'rss feed' do + let(:access_level) { ProjectFeature::ENABLED } + + context 'when signed in' do + let(:user) { user_in_group } + + it_behaves_like "it has an RSS button with current_user's private token" + it_behaves_like "an autodiscoverable RSS feed with current_user's private token" + end + + context 'when signed out' do + let(:user) { nil } + + it_behaves_like "it has an RSS button without a private token" + it_behaves_like "an autodiscoverable RSS feed without a private token" + end + end end diff --git a/spec/features/groups/members/list_spec.rb b/spec/features/groups/members/list_spec.rb new file mode 100644 index 00000000000..14c193f7450 --- /dev/null +++ b/spec/features/groups/members/list_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +feature 'Groups members list', feature: true do + let(:user1) { create(:user, name: 'John Doe') } + let(:user2) { create(:user, name: 'Mary Jane') } + let(:group) { create(:group) } + let(:nested_group) { create(:group, parent: group) } + + background do + login_as(user1) + end + + scenario 'show members from current group and parent' do + group.add_developer(user1) + nested_group.add_developer(user2) + + visit group_group_members_path(nested_group) + + expect(first_row.text).to include(user1.name) + expect(second_row.text).to include(user2.name) + end + + scenario 'show user once if member of both current group and parent' do + group.add_developer(user1) + nested_group.add_developer(user1) + + visit group_group_members_path(nested_group) + + expect(first_row.text).to include(user1.name) + expect(second_row).to be_blank + end + + it 'updates user to owner level', :js do + group.add_owner(user1) + group.add_developer(user2) + + visit group_group_members_path(group) + + page.within(second_row) do + click_button('Developer') + + click_link('Owner') + + expect(page).to have_button('Owner') + end + end + + def first_row + page.all('ul.content-list > li')[0] + end + + def second_row + page.all('ul.content-list > li')[1] + end +end diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb new file mode 100644 index 00000000000..fb39693e8ca --- /dev/null +++ b/spec/features/groups/show_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +feature 'Group show page', feature: true do + let(:group) { create(:group) } + let(:path) { group_path(group) } + + context 'when signed in' do + before do + user = create(:group_member, :developer, user: create(:user), group: group ).user + login_as(user) + visit path + end + + it_behaves_like "an autodiscoverable RSS feed with current_user's private token" + end + + context 'when signed out' do + before do + visit path + end + + it_behaves_like "an autodiscoverable RSS feed without a private token" + end +end diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb index 40a1fced8d8..e0b2404e60a 100644 --- a/spec/features/help_pages_spec.rb +++ b/spec/features/help_pages_spec.rb @@ -33,4 +33,30 @@ describe 'Help Pages', feature: true do it_behaves_like 'help page', prefix: '/gitlab' end end + + context 'in a production environment with version check enabled', js: true do + before do + allow(Rails.env).to receive(:production?) { true } + allow(current_application_settings).to receive(:version_check_enabled) { true } + allow_any_instance_of(VersionCheck).to receive(:url) { '/version-check-url' } + + login_as :user + visit help_path + end + + it 'should display a version check image' do + expect(find('.js-version-status-badge')).to be_visible + end + + it 'should have a src url' do + expect(find('.js-version-status-badge')['src']).to match(/\/version-check-url/) + end + + it 'should hide the version check image if the image request fails' do + # We use '--load-images=no' with poltergeist so we must trigger manually + execute_script("$('.js-version-status-badge').trigger('error');") + + expect(find('.js-version-status-badge', visible: false)).not_to be_visible + end + end end diff --git a/spec/features/issuables/issuable_list_spec.rb b/spec/features/issuables/issuable_list_spec.rb new file mode 100644 index 00000000000..b90bf6268fd --- /dev/null +++ b/spec/features/issuables/issuable_list_spec.rb @@ -0,0 +1,75 @@ +require 'rails_helper' + +describe 'issuable list', feature: true do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + + issuable_types = [:issue, :merge_request] + + before do + project.add_user(user, :developer) + login_as(user) + issuable_types.each { |type| create_issuables(type) } + end + + issuable_types.each do |issuable_type| + it "avoids N+1 database queries for #{issuable_type.to_s.humanize.pluralize}" do + control_count = ActiveRecord::QueryRecorder.new { visit_issuable_list(issuable_type) }.count + + create_issuables(issuable_type) + + expect { visit_issuable_list(issuable_type) }.not_to exceed_query_limit(control_count) + end + + it "counts upvotes, downvotes and notes count for each #{issuable_type.to_s.humanize}" do + visit_issuable_list(issuable_type) + + expect(first('.fa-thumbs-up').find(:xpath, '..')).to have_content(1) + expect(first('.fa-thumbs-down').find(:xpath, '..')).to have_content(1) + expect(first('.fa-comments').find(:xpath, '..')).to have_content(2) + end + end + + it "counts merge requests closing issues icons for each issue" do + visit_issuable_list(:issue) + + expect(page).to have_selector('.icon-merge-request-unmerged', count: 1) + expect(first('.icon-merge-request-unmerged').find(:xpath, '..')).to have_content(1) + end + + def visit_issuable_list(issuable_type) + if issuable_type == :issue + visit namespace_project_issues_path(project.namespace, project) + else + visit namespace_project_merge_requests_path(project.namespace, project) + end + end + + def create_issuables(issuable_type) + 3.times do + issuable = + if issuable_type == :issue + create(:issue, project: project, author: user) + else + create(:merge_request, title: FFaker::Lorem.sentence, source_project: project, source_branch: FFaker::Name.name) + end + + 2.times do + create(:note_on_issue, noteable: issuable, project: project, note: 'Test note') + end + + create(:award_emoji, :downvote, awardable: issuable) + create(:award_emoji, :upvote, awardable: issuable) + end + + if issuable_type == :issue + issue = Issue.reorder(:iid).first + merge_request = create(:merge_request, + title: FFaker::Lorem.sentence, + source_project: project, + source_branch: FFaker::Name.name) + + MergeRequestsClosingIssues.create!(issue: issue, merge_request: merge_request) + end + end +end diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb index 73e43316dc7..3ab3d2d4229 100644 --- a/spec/features/issues/award_emoji_spec.rb +++ b/spec/features/issues/award_emoji_spec.rb @@ -67,6 +67,18 @@ describe 'Awards Emoji', feature: true do expect(page).not_to have_selector(emoji_counter) end end + + context 'execute /award slash command' do + it 'toggles the emoji award on noteable', js: true do + execute_slash_command('/award :100:') + + expect(find(noteable_award_counter)).to have_text("1") + + execute_slash_command('/award :100:') + + expect(page).not_to have_selector(noteable_award_counter) + end + end end end @@ -80,6 +92,15 @@ describe 'Awards Emoji', feature: true do end end + def execute_slash_command(cmd) + within('.js-main-target-form') do + fill_in 'note[note]', with: cmd + click_button 'Comment' + end + + wait_for_ajax + end + def thumbsup_emoji page.all(emoji_counter).first end @@ -92,6 +113,10 @@ describe 'Awards Emoji', feature: true do 'span.js-counter' end + def noteable_award_counter + ".awards .active" + end + def toggle_smiley_emoji(status) within('.note') do find('.note-emoji-button').click diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb index 832757b24d4..2f59630b4fb 100644 --- a/spec/features/issues/bulk_assignment_labels_spec.rb +++ b/spec/features/issues/bulk_assignment_labels_spec.rb @@ -55,7 +55,7 @@ feature 'Issues > Labels bulk assignment', feature: true do context 'to all issues' do before do check 'check_all_issues' - open_labels_dropdown ['bug', 'feature'] + open_labels_dropdown %w(bug feature) update_issues end @@ -70,7 +70,7 @@ feature 'Issues > Labels bulk assignment', feature: true do context 'to a issue' do before do check "selected_issue_#{issue1.id}" - open_labels_dropdown ['bug', 'feature'] + open_labels_dropdown %w(bug feature) update_issues end @@ -112,7 +112,7 @@ feature 'Issues > Labels bulk assignment', feature: true do visit namespace_project_issues_path(project.namespace, project) check 'check_all_issues' - unmark_labels_in_dropdown ['bug', 'feature'] + unmark_labels_in_dropdown %w(bug feature) update_issues end diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb index c6a88e1b7b0..ab3b868fd3a 100644 --- a/spec/features/issues/filtered_search/dropdown_label_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe 'Dropdown label', js: true, feature: true do + include FilteredSearchHelpers + let(:project) { create(:empty_project) } let(:user) { create(:user) } let(:filtered_search) { find('.filtered-search') } @@ -17,12 +19,6 @@ describe 'Dropdown label', js: true, feature: true do let!(:long_label) { create(:label, project: project, title: 'this is a very long title this is a very long title this is a very long title this is a very long title this is a very long title') } end - def init_label_search - filtered_search.set('label:') - # This ensures the dropdown is shown - expect(find(js_dropdown_label)).not_to have_css('.filter-dropdown-loading') - end - def search_for_label(label) init_label_search filtered_search.send_keys(label) diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index 6f7046c8461..0420e64d42c 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -1,6 +1,7 @@ require 'rails_helper' describe 'Filter issues', js: true, feature: true do + include FilteredSearchHelpers include WaitForAjax let!(:group) { create(:group) } @@ -17,19 +18,6 @@ describe 'Filter issues', js: true, feature: true do let!(:multiple_words_label) { create(:label, project: project, title: "Two words") } let!(:closed_issue) { create(:issue, title: 'bug that is closed', project: project, state: :closed) } - let(:filtered_search) { find('.filtered-search') } - - def input_filtered_search(search_term, submit: true) - filtered_search.set(search_term) - - if submit - filtered_search.send_keys(:enter) - end - end - - def expect_filtered_search_input(input) - expect(find('.filtered-search').value).to eq(input) - end def expect_no_issues_list page.within '.issues-list' do @@ -113,13 +101,11 @@ describe 'Filter issues', js: true, feature: true do end it 'filters issues by invalid author' do - pending('to be tested, issue #26546') - expect(true).to be(false) + skip('to be tested, issue #26546') end it 'filters issues by multiple authors' do - pending('to be tested, issue #26546') - expect(true).to be(false) + skip('to be tested, issue #26546') end end @@ -158,8 +144,7 @@ describe 'Filter issues', js: true, feature: true do end it 'sorting' do - pending('to be tested, issue #26546') - expect(true).to be(false) + skip('to be tested, issue #26546') end end @@ -182,13 +167,11 @@ describe 'Filter issues', js: true, feature: true do end it 'filters issues by invalid assignee' do - pending('to be tested, issue #26546') - expect(true).to be(false) + skip('to be tested, issue #26546') end it 'filters issues by multiple assignees' do - pending('to be tested, issue #26546') - expect(true).to be(false) + skip('to be tested, issue #26546') end end @@ -228,8 +211,7 @@ describe 'Filter issues', js: true, feature: true do context 'sorting' do it 'sorts' do - pending('to be tested, issue #26546') - expect(true).to be(false) + skip('to be tested, issue #26546') end end end @@ -253,8 +235,7 @@ describe 'Filter issues', js: true, feature: true do end it 'filters issues by invalid label' do - pending('to be tested, issue #26546') - expect(true).to be(false) + skip('to be tested, issue #26546') end it 'filters issues by multiple labels' do @@ -429,8 +410,7 @@ describe 'Filter issues', js: true, feature: true do context 'sorting' do it 'sorts' do - pending('to be tested, issue #26546') - expect(true).to be(false) + skip('to be tested, issue #26546') end end end @@ -456,13 +436,11 @@ describe 'Filter issues', js: true, feature: true do end it 'filters issues by invalid milestones' do - pending('to be tested, issue #26546') - expect(true).to be(false) + skip('to be tested, issue #26546') end it 'filters issues by multiple milestones' do - pending('to be tested, issue #26546') - expect(true).to be(false) + skip('to be tested, issue #26546') end it 'filters issues by milestone containing special characters' do @@ -523,8 +501,7 @@ describe 'Filter issues', js: true, feature: true do context 'sorting' do it 'sorts' do - pending('to be tested, issue #26546') - expect(true).to be(false) + skip('to be tested, issue #26546') end end end diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index 93139dc9e94..7135565294b 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -182,6 +182,20 @@ feature 'GFM autocomplete', feature: true, js: true do expect(page).not_to have_selector('.atwho-view') end + it 'triggers autocomplete after selecting a slash command' do + note = find('#note_note') + page.within '.timeline-content-form' do + note.native.send_keys('') + note.native.send_keys('/as') + note.click + end + + find('.atwho-view li', text: '/assign').native.send_keys(:tab) + + user_item = find('.atwho-view li', text: user.username) + expect(user_item).to have_content(user.username) + end + def expect_to_wrap(should_wrap, item, note, value) expect(item).to have_content(value) expect(item).not_to have_content("\"#{value}\"") diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb index 1eb981942ea..7b9d4534ada 100644 --- a/spec/features/issues/issue_sidebar_spec.rb +++ b/spec/features/issues/issue_sidebar_spec.rb @@ -7,9 +7,9 @@ feature 'Issue Sidebar', feature: true do let(:project) { create(:project, :public) } let(:issue) { create(:issue, project: project) } let!(:user) { create(:user)} + let!(:label) { create(:label, project: project, title: 'bug') } before do - create(:label, project: project, title: 'bug') login_as(user) end @@ -50,16 +50,6 @@ feature 'Issue Sidebar', feature: true do visit_issue(project, issue) end - describe 'when clicking on edit labels', js: true do - it 'shows dropdown option to create a new label' do - find('.block.labels .edit-link').click - - page.within('.block.labels') do - expect(page).to have_content 'Create new' - end - end - end - context 'sidebar', js: true do it 'changes size when the screen size is smaller' do sidebar_selector = 'aside.right-sidebar.right-sidebar-collapsed' @@ -77,36 +67,53 @@ feature 'Issue Sidebar', feature: true do end end - context 'creating a new label', js: true do - it 'shows option to crate a new label is present' do + context 'editing issue labels', js: true do + before do page.within('.block.labels') do find('.edit-link').click + end + end + it 'shows option to create a new label' do + page.within('.block.labels') do expect(page).to have_content 'Create new' end end - it 'shows dropdown switches to "create label" section' do - page.within('.block.labels') do - find('.edit-link').click - click_link 'Create new' + context 'creating a new label', js: true do + before do + page.within('.block.labels') do + click_link 'Create new' + end + end - expect(page).to have_content 'Create new label' + it 'shows dropdown switches to "create label" section' do + page.within('.block.labels') do + expect(page).to have_content 'Create new label' + end end - end - it 'adds new label' do - page.within('.block.labels') do - find('.edit-link').click - sleep 1 - click_link 'Create new' + it 'adds new label' do + page.within('.block.labels') do + fill_in 'new_label_name', with: 'wontfix' + page.find(".suggest-colors a", match: :first).click + click_button 'Create' + + page.within('.dropdown-page-one') do + expect(page).to have_content 'wontfix' + end + end + end - fill_in 'new_label_name', with: 'wontfix' - page.find(".suggest-colors a", match: :first).click - click_button 'Create' + it 'shows error message if label title is taken' do + page.within('.block.labels') do + fill_in 'new_label_name', with: label.title + page.find('.suggest-colors a', match: :first).click + click_button 'Create' - page.within('.dropdown-page-one') do - expect(page).to have_content 'wontfix' + page.within('.dropdown-page-two') do + expect(page).to have_content 'Title has already been taken' + end end end end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 755162a1eb5..1c8267b1593 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe 'Issues', feature: true do + include DropzoneHelper include IssueHelpers include SortingHelper include WaitForAjax @@ -150,7 +151,7 @@ describe 'Issues', feature: true do describe 'Filter issue' do before do - ['foobar', 'barbaz', 'gitlab'].each do |title| + %w(foobar barbaz gitlab).each do |title| create(:issue, author: @user, assignee: @user, @@ -382,7 +383,9 @@ describe 'Issues', feature: true do it 'changes incoming email address token', js: true do find('.issue-email-modal-btn').click previous_token = find('input#issue_email').value - find('.incoming-email-token-reset').click + find('.incoming-email-token-reset').trigger('click') + + wait_for_ajax expect(page).to have_no_field('issue_email', with: previous_token) new_token = project1.new_issue_address(@user.reload) @@ -568,13 +571,16 @@ describe 'Issues', feature: true do end it 'uploads file when dragging into textarea' do - drop_in_dropzone test_image_file - - # Wait for the file to upload - sleep 1 + dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif') expect(page.find_field("issue_description").value).to have_content 'banana_sample' end + + it 'adds double newline to end of attachment markdown' do + dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif') + + expect(page.find_field("issue_description").value).to match /\n\n$/ + end end end @@ -636,7 +642,7 @@ describe 'Issues', feature: true do it 'removes due date from issue' do date = Date.today.at_beginning_of_month + 2.days - + page.within '.due_date' do click_link 'Edit' @@ -654,25 +660,4 @@ describe 'Issues', feature: true do end end end - - def drop_in_dropzone(file_path) - # Generate a fake input selector - page.execute_script <<-JS - var fakeFileInput = window.$('<input/>').attr( - {id: 'fakeFileInput', type: 'file'} - ).appendTo('body'); - JS - # Attach the file to the fake input selector with Capybara - attach_file("fakeFileInput", file_path) - # Add the file to a fileList array and trigger the fake drop event - page.execute_script <<-JS - var fileList = [$('#fakeFileInput')[0].files[0]]; - var e = jQuery.Event('drop', { dataTransfer : { files : fileList } }); - $('.div-dropzone')[0].dropzone.listeners[0].events.drop(e); - JS - end - - def test_image_file - File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') - end end diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb index ab7d89306db..ae609160e18 100644 --- a/spec/features/login_spec.rb +++ b/spec/features/login_spec.rb @@ -32,6 +32,22 @@ feature 'Login', feature: true do end end + describe 'with a blocked account' do + it 'prevents the user from logging in' do + user = create(:user, :blocked) + + login_with(user) + + expect(page).to have_content('Your account has been blocked.') + end + + it 'does not update Devise trackable attributes' do + user = create(:user, :blocked) + + expect { login_with(user) }.not_to change { user.reload.sign_in_count } + end + end + describe 'with two-factor authentication' do def enter_code(code) fill_in 'user_otp_attempt', with: code diff --git a/spec/features/merge_requests/closes_issues_spec.rb b/spec/features/merge_requests/closes_issues_spec.rb index c73065cdce1..eafcab6a0d7 100644 --- a/spec/features/merge_requests/closes_issues_spec.rb +++ b/spec/features/merge_requests/closes_issues_spec.rb @@ -10,10 +10,12 @@ feature 'Merge Request closing issues message', feature: true do :merge_request, :simple, source_project: project, - description: merge_request_description + description: merge_request_description, + title: merge_request_title ) end let(:merge_request_description) { 'Merge Request Description' } + let(:merge_request_title) { 'Merge Request Title' } before do project.team << [user, :master] @@ -45,8 +47,32 @@ feature 'Merge Request closing issues message', feature: true do end end - context 'closing some issues and mentioning, but not closing, others' do - let(:merge_request_description) { "Description\n\ncloses #{issue_1.to_reference}\n\n refers to #{issue_2.to_reference}" } + context 'closing some issues in title and mentioning, but not closing, others' do + let(:merge_request_title) { "closes #{issue_1.to_reference}\n\n refers to #{issue_2.to_reference}" } + + it 'does not display closing issue message' do + expect(page).to have_content("Accepting this merge request will close issue #{issue_1.to_reference}. Issue #{issue_2.to_reference} is mentioned but will not be closed.") + end + end + + context 'closing issues using title but not mentioning any other issue' do + let(:merge_request_title) { "closing #{issue_1.to_reference}, #{issue_2.to_reference}" } + + it 'does not display closing issue message' do + expect(page).to have_content("Accepting this merge request will close issues #{issue_1.to_reference} and #{issue_2.to_reference}") + end + end + + context 'mentioning issues using title but not closing them' do + let(:merge_request_title) { "Refers to #{issue_1.to_reference} and #{issue_2.to_reference}" } + + it 'does not display closing issue message' do + expect(page).to have_content("Issues #{issue_1.to_reference} and #{issue_2.to_reference} are mentioned but will not be closed.") + end + end + + context 'closing some issues using title and mentioning, but not closing, others' do + let(:merge_request_title) { "closes #{issue_1.to_reference}\n\n refers to #{issue_2.to_reference}" } it 'does not display closing issue message' do expect(page).to have_content("Accepting this merge request will close issue #{issue_1.to_reference}. Issue #{issue_2.to_reference} is mentioned but will not be closed.") diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb index 5bc4ab2dfe5..18508a44184 100644 --- a/spec/features/merge_requests/conflicts_spec.rb +++ b/spec/features/merge_requests/conflicts_spec.rb @@ -141,7 +141,7 @@ feature 'Merge request conflict resolution', js: true, feature: true do click_on 'Changes' wait_for_ajax - find('.click-to-expand').click + click_link 'Expand all' wait_for_ajax expect(page).to have_content('Gregor Samsa woke from troubled dreams') @@ -154,7 +154,7 @@ feature 'Merge request conflict resolution', js: true, feature: true do 'conflict-binary-file' => 'when the conflicts contain a binary file', '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', - } + }.freeze UNRESOLVABLE_CONFLICTS.each do |source_branch, description| context description do diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb index e853fb7e016..0832a3656a8 100644 --- a/spec/features/merge_requests/create_new_mr_spec.rb +++ b/spec/features/merge_requests/create_new_mr_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' feature 'Create New Merge Request', feature: true, js: true do + include WaitForVueResource + let(:user) { create(:user) } let(:project) { create(:project, :public) } @@ -99,6 +101,7 @@ feature 'Create New Merge Request', feature: true, js: true do page.within('.merge-request') do click_link 'Pipelines' + wait_for_vue_resource expect(page).to have_content "##{pipeline.id}" end diff --git a/spec/features/merge_requests/filter_by_labels_spec.rb b/spec/features/merge_requests/filter_by_labels_spec.rb index 4c60329865c..55f3c1863ff 100644 --- a/spec/features/merge_requests/filter_by_labels_spec.rb +++ b/spec/features/merge_requests/filter_by_labels_spec.rb @@ -1,6 +1,8 @@ require 'rails_helper' feature 'Issue filtering by Labels', feature: true, js: true do + include FilteredSearchHelpers + include MergeRequestHelpers include WaitForAjax let(:project) { create(:project, :public) } @@ -32,123 +34,77 @@ feature 'Issue filtering by Labels', feature: true, js: true do context 'filter by label bug' do before do - select_labels('bug') + input_filtered_search('label:~bug') end it 'apply the filter' do expect(page).to have_content "Bugfix1" expect(page).to have_content "Bugfix2" expect(page).not_to have_content "Feature1" - expect(find('.filtered-labels')).to have_content "bug" - expect(find('.filtered-labels')).not_to have_content "feature" - expect(find('.filtered-labels')).not_to have_content "enhancement" - - find('.js-label-filter-remove').click - wait_for_ajax - expect(find('.filtered-labels', visible: false)).to have_no_content "bug" end end context 'filter by label feature' do before do - select_labels('feature') + input_filtered_search('label:~feature') end it 'applies the filter' do expect(page).to have_content "Feature1" expect(page).not_to have_content "Bugfix2" expect(page).not_to have_content "Bugfix1" - expect(find('.filtered-labels')).to have_content "feature" - expect(find('.filtered-labels')).not_to have_content "bug" - expect(find('.filtered-labels')).not_to have_content "enhancement" end end context 'filter by label enhancement' do before do - select_labels('enhancement') + input_filtered_search('label:~enhancement') end it 'applies the filter' do expect(page).to have_content "Bugfix2" expect(page).not_to have_content "Feature1" expect(page).not_to have_content "Bugfix1" - expect(find('.filtered-labels')).to have_content "enhancement" - expect(find('.filtered-labels')).not_to have_content "bug" - expect(find('.filtered-labels')).not_to have_content "feature" end end context 'filter by label enhancement and bug in issues list' do before do - select_labels('bug', 'enhancement') + input_filtered_search('label:~bug label:~enhancement') end it 'applies the filters' do expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_content "Bugfix2" expect(page).not_to have_content "Feature1" - expect(find('.filtered-labels')).to have_content "bug" - expect(find('.filtered-labels')).to have_content "enhancement" - expect(find('.filtered-labels')).not_to have_content "feature" - - find('.js-label-filter-remove', match: :first).click - wait_for_ajax - - expect(page).to have_content "Bugfix2" - expect(page).not_to have_content "Feature1" - expect(page).not_to have_content "Bugfix1" - expect(find('.filtered-labels')).not_to have_content "bug" - expect(find('.filtered-labels')).to have_content "enhancement" - expect(find('.filtered-labels')).not_to have_content "feature" end end - context 'remove filtered labels' do + context 'clear button' do before do - page.within '.labels-filter' do - click_button 'Label' - wait_for_ajax - click_link 'bug' - find('.dropdown-menu-close').click - end - - page.within '.filtered-labels' do - expect(page).to have_content 'bug' - end + input_filtered_search('label:~bug') end it 'allows user to remove filtered labels' do - first('.js-label-filter-remove').click - wait_for_ajax + first('.clear-search').click + filtered_search.send_keys(:enter) - expect(find('.filtered-labels', visible: false)).not_to have_content 'bug' - expect(find('.labels-filter')).not_to have_content 'bug' + expect(page).to have_issuable_counts(open: 3, closed: 0, all: 3) + expect(page).to have_content "Bugfix2" + expect(page).to have_content "Feature1" + expect(page).to have_content "Bugfix1" end end - context 'dropdown filtering' do + context 'filter dropdown' do it 'filters by label name' do - page.within '.labels-filter' do - click_button 'Label' - wait_for_ajax - find('.dropdown-input input').set 'bug' - - page.within '.dropdown-content' do - expect(page).not_to have_content 'enhancement' - expect(page).to have_content 'bug' - end - end - end - end + init_label_search + filtered_search.send_keys('~bug') - def select_labels(*labels) - page.find('.js-label-select').click - wait_for_ajax - labels.each do |label| - execute_script("$('.dropdown-menu-labels li:contains(\"#{label}\") a').click()") + page.within '.filter-dropdown' do + expect(page).not_to have_content 'enhancement' + expect(page).to have_content 'bug' + end end - page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click - wait_for_ajax end end diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb index f6e9230c8da..5608cda28f8 100644 --- a/spec/features/merge_requests/filter_by_milestone_spec.rb +++ b/spec/features/merge_requests/filter_by_milestone_spec.rb @@ -1,10 +1,18 @@ require 'rails_helper' feature 'Merge Request filtering by Milestone', feature: true do + include FilteredSearchHelpers + include MergeRequestHelpers + let(:project) { create(:project, :public) } let!(:user) { create(:user)} let(:milestone) { create(:milestone, project: project) } + def filter_by_milestone(title) + find(".js-milestone-select").click + find(".milestone-filter a", text: title).click + end + before do project.team << [user, :master] login_as(user) @@ -15,42 +23,42 @@ feature 'Merge Request filtering by Milestone', feature: true do create(:merge_request, :simple, source_project: project, milestone: milestone) visit_merge_requests(project) - filter_by_milestone(Milestone::None.title) + input_filtered_search('milestone:none') expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_css('.merge-request', count: 1) end context 'filters by upcoming milestone', js: true do - it 'does not show issues with no expiry' do + it 'does not show merge requests with no expiry' do create(:merge_request, :with_diffs, source_project: project) create(:merge_request, :simple, source_project: project, milestone: milestone) visit_merge_requests(project) - filter_by_milestone(Milestone::Upcoming.title) + input_filtered_search('milestone:upcoming') expect(page).to have_css('.merge-request', count: 0) end - it 'shows issues in future' do + it 'shows merge requests in future' do milestone = create(:milestone, project: project, due_date: Date.tomorrow) create(:merge_request, :with_diffs, source_project: project) create(:merge_request, :simple, source_project: project, milestone: milestone) visit_merge_requests(project) - filter_by_milestone(Milestone::Upcoming.title) + input_filtered_search('milestone:upcoming') expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_css('.merge-request', count: 1) end - it 'does not show issues in past' do + it 'does not show merge requests in past' do milestone = create(:milestone, project: project, due_date: Date.yesterday) create(:merge_request, :with_diffs, source_project: project) create(:merge_request, :simple, source_project: project, milestone: milestone) visit_merge_requests(project) - filter_by_milestone(Milestone::Upcoming.title) + input_filtered_search('milestone:upcoming') expect(page).to have_css('.merge-request', count: 0) end @@ -61,7 +69,7 @@ feature 'Merge Request filtering by Milestone', feature: true do create(:merge_request, :simple, source_project: project) visit_merge_requests(project) - filter_by_milestone(milestone.title) + input_filtered_search("milestone:%'#{milestone.title}'") expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_css('.merge-request', count: 1) @@ -77,19 +85,10 @@ feature 'Merge Request filtering by Milestone', feature: true do create(:merge_request, :simple, source_project: project) visit_merge_requests(project) - filter_by_milestone(milestone.title) + input_filtered_search("milestone:%\"#{milestone.title}\"") expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_css('.merge-request', count: 1) end end - - def visit_merge_requests(project) - visit namespace_project_merge_requests_path(project.namespace, project) - end - - def filter_by_milestone(title) - find(".js-milestone-select").click - find(".milestone-filter a", text: title).click - end end diff --git a/spec/features/merge_requests/filter_merge_requests_spec.rb b/spec/features/merge_requests/filter_merge_requests_spec.rb index 4642b5a530d..6579a88d4ab 100644 --- a/spec/features/merge_requests/filter_merge_requests_spec.rb +++ b/spec/features/merge_requests/filter_merge_requests_spec.rb @@ -1,11 +1,13 @@ require 'rails_helper' describe 'Filter merge requests', feature: true do + include FilteredSearchHelpers + include MergeRequestHelpers include WaitForAjax let!(:project) { create(:project) } let!(:group) { create(:group) } - let!(:user) { create(:user)} + let!(:user) { create(:user) } let!(:milestone) { create(:milestone, project: project) } let!(:label) { create(:label, project: project) } let!(:wontfix) { create(:label, project: project, title: "Won't fix") } @@ -15,183 +17,134 @@ describe 'Filter merge requests', feature: true do group.add_developer(user) login_as(user) create(:merge_request, source_project: project, target_project: project) + + visit namespace_project_merge_requests_path(project.namespace, project) end describe 'for assignee from mr#index' do - before do - visit namespace_project_merge_requests_path(project.namespace, project) + let(:search_query) { "assignee:@#{user.username}" } - find('.js-assignee-search').click - - find('.dropdown-menu-user-link', text: user.username).click + before do + input_filtered_search(search_query) - wait_for_ajax + expect_mr_list_count(0) end context 'assignee', js: true do it 'updates to current user' do - expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + expect_filtered_search_input(search_query) end it 'does not change when closed link is clicked' do find('.issues-state-filters a', text: "Closed").click - expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + expect_filtered_search_input(search_query) end it 'does not change when all link is clicked' do find('.issues-state-filters a', text: "All").click - expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + expect_filtered_search_input(search_query) end end end describe 'for milestone from mr#index' do - before do - visit namespace_project_merge_requests_path(project.namespace, project) - - find('.js-milestone-select').click + let(:search_query) { "milestone:%#{milestone.title}" } - find('.milestone-filter .dropdown-content a', text: milestone.title).click + before do + input_filtered_search(search_query) - wait_for_ajax + expect_mr_list_count(0) end context 'milestone', js: true do it 'updates to current milestone' do - expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title) + expect_filtered_search_input(search_query) end it 'does not change when closed link is clicked' do find('.issues-state-filters a', text: "Closed").click - expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title) + expect_filtered_search_input(search_query) end it 'does not change when all link is clicked' do find('.issues-state-filters a', text: "All").click - expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title) + expect_filtered_search_input(search_query) end end end describe 'for label from mr#index', js: true do - before do - visit namespace_project_merge_requests_path(project.namespace, project) - find('.js-label-select').click - wait_for_ajax - end - - it 'filters by any label' do - find('.dropdown-menu-labels a', text: 'Any Label').click - page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click - wait_for_ajax - - expect(find('.labels-filter')).to have_content 'Label' - end - it 'filters by no label' do - find('.dropdown-menu-labels a', text: 'No Label').click - page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click - wait_for_ajax + input_filtered_search('label:none') - page.within '.labels-filter' do - expect(page).to have_content 'Labels' - end - expect(find('.js-label-select .dropdown-toggle-text')).to have_content('Labels') + expect_mr_list_count(1) + expect_filtered_search_input('label:none') end it 'filters by a label' do - find('.dropdown-menu-labels a', text: label.title).click - page.within '.labels-filter' do - expect(page).to have_content label.title - end - expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title) + input_filtered_search("label:~#{label.title}") + + expect_mr_list_count(0) + expect_filtered_search_input("label:~#{label.title}") end it "filters by `won't fix` and another label" do - page.within '.labels-filter' do - click_link wontfix.title - expect(page).to have_content wontfix.title - click_link label.title - end + input_filtered_search("label:~\"#{wontfix.title}\" label:~#{label.title}") - expect(find('.js-label-select .dropdown-toggle-text')).to have_content("#{wontfix.title} +1 more") + expect_mr_list_count(0) + expect_filtered_search_input("label:~\"#{wontfix.title}\" label:~#{label.title}") end it "filters by `won't fix` label followed by another label after page load" do - page.within '.labels-filter' do - click_link wontfix.title - expect(page).to have_content wontfix.title - end - - find('body').click - - expect(find('.filtered-labels')).to have_content(wontfix.title) - - find('.js-label-select').click - wait_for_ajax - find('.dropdown-menu-labels a', text: label.title).click - - find('body').click + input_filtered_search("label:~\"#{wontfix.title}\"") - expect(find('.filtered-labels')).to have_content(wontfix.title) - expect(find('.filtered-labels')).to have_content(label.title) + expect_mr_list_count(0) + expect_filtered_search_input("label:~\"#{wontfix.title}\"") - find('.js-label-select').click - wait_for_ajax + input_filtered_search_keys(" label:~#{label.title}") - expect(find('.dropdown-menu-labels li', text: wontfix.title)).to have_css('.is-active') - expect(find('.dropdown-menu-labels li', text: label.title)).to have_css('.is-active') - end + expect_filtered_search_input("label:~\"#{wontfix.title}\" label:~#{label.title}") - it "selects and unselects `won't fix`" do - find('.dropdown-menu-labels a', text: wontfix.title).click - find('.dropdown-menu-labels a', text: wontfix.title).click - # Close label dropdown to load - find('body').click - expect(page).not_to have_css('.filtered-labels') + expect_mr_list_count(0) + expect_filtered_search_input("label:~\"#{wontfix.title}\" label:~#{label.title}") end end describe 'for assignee and label from issues#index' do - before do - visit namespace_project_merge_requests_path(project.namespace, project) - - find('.js-assignee-search').click + let(:search_query) { "assignee:@#{user.username} label:~#{label.title}" } - find('.dropdown-menu-user-link', text: user.username).click + before do + input_filtered_search("assignee:@#{user.username}") - expect(page).not_to have_selector('.mr-list .merge-request') + expect_mr_list_count(1) + expect_filtered_search_input("assignee:@#{user.username}") - find('.js-label-select').click + input_filtered_search_keys(" label:~#{label.title}") - find('.dropdown-menu-labels .dropdown-content a', text: label.title).click - page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click + expect_mr_list_count(1) - wait_for_ajax + find("#state-opened[href=\"#{URI.parse(current_url).path}?assignee_username=#{user.username}&label_name%5B%5D=#{label.title}&scope=all&state=opened\"]") end context 'assignee and label', js: true do it 'updates to current assignee and label' do - expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) - expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title) + expect_filtered_search_input(search_query) end it 'does not change when closed link is clicked' do find('.issues-state-filters a', text: "Closed").click - expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) - expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title) + expect_filtered_search_input(search_query) end it 'does not change when all link is clicked' do find('.issues-state-filters a', text: "All").click - expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) - expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title) + expect_filtered_search_input(search_query) end end end @@ -203,11 +156,11 @@ describe 'Filter merge requests', feature: true do bug_label = create(:label, project: project, title: 'bug') milestone = create(:milestone, title: "8", project: project) - mr = create(:merge_request, - title: "Bug 2", - source_project: project, - target_project: project, - source_branch: "bug2", + mr = create(:merge_request, + title: "Bug 2", + source_project: project, + target_project: project, + source_branch: "bug2", milestone: milestone, author: user, assignee: user) @@ -218,15 +171,13 @@ describe 'Filter merge requests', feature: true do context 'only text', js: true do it 'filters merge requests by searched text' do - fill_in 'issuable_search', with: 'Bug' + input_filtered_search('bug') - page.within '.mr-list' do - expect(page).to have_selector('.merge-request', count: 2) - end + expect_mr_list_count(2) end it 'does not show any merge requests' do - fill_in 'issuable_search', with: 'testing' + input_filtered_search('testing') page.within '.mr-list' do expect(page).not_to have_selector('.merge-request') @@ -234,82 +185,49 @@ describe 'Filter merge requests', feature: true do end end - context 'text and dropdown options', js: true do + context 'filters and searches', js: true do it 'filters by text and label' do - fill_in 'issuable_search', with: 'Bug' + input_filtered_search('Bug') - expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) - page.within '.mr-list' do - expect(page).to have_selector('.merge-request', count: 2) - end + expect_mr_list_count(2) + expect_filtered_search_input('Bug') - click_button 'Label' - page.within '.labels-filter' do - click_link 'bug' - end - find('.dropdown-menu-close-icon').click + input_filtered_search_keys(' label:~bug') - expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) - page.within '.mr-list' do - expect(page).to have_selector('.merge-request', count: 1) - end + expect_mr_list_count(1) end it 'filters by text and milestone' do - fill_in 'issuable_search', with: 'Bug' + input_filtered_search('Bug') - expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) - page.within '.mr-list' do - expect(page).to have_selector('.merge-request', count: 2) - end + expect_mr_list_count(2) + expect_filtered_search_input('Bug') - click_button 'Milestone' - page.within '.milestone-filter' do - click_link '8' - end + input_filtered_search_keys(' milestone:%8') - expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) - page.within '.mr-list' do - expect(page).to have_selector('.merge-request', count: 1) - end + expect_mr_list_count(1) end it 'filters by text and assignee' do - fill_in 'issuable_search', with: 'Bug' + input_filtered_search('Bug') - expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) - page.within '.mr-list' do - expect(page).to have_selector('.merge-request', count: 2) - end + expect_mr_list_count(2) + expect_filtered_search_input('Bug') - click_button 'Assignee' - page.within '.dropdown-menu-assignee' do - click_link user.name - end + input_filtered_search_keys(" assignee:@#{user.username}") - expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) - page.within '.mr-list' do - expect(page).to have_selector('.merge-request', count: 1) - end + expect_mr_list_count(1) end it 'filters by text and author' do - fill_in 'issuable_search', with: 'Bug' + input_filtered_search('Bug') - expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) - page.within '.mr-list' do - expect(page).to have_selector('.merge-request', count: 2) - end + expect_mr_list_count(2) + expect_filtered_search_input('Bug') - click_button 'Author' - page.within '.dropdown-menu-author' do - click_link user.name - end + input_filtered_search_keys(" author:@#{user.username}") - expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) - page.within '.mr-list' do - expect(page).to have_selector('.merge-request', count: 1) - end + expect_mr_list_count(1) end end end @@ -328,18 +246,9 @@ describe 'Filter merge requests', feature: true do end it 'is able to filter and sort merge requests' do - click_button 'Label' - wait_for_ajax - page.within '.labels-filter' do - click_link 'bug' - end - find('.dropdown-menu-close-icon').click - wait_for_ajax + input_filtered_search('label:~bug') - expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) - page.within '.mr-list' do - expect(page).to have_selector('.merge-request', count: 2) - end + expect_mr_list_count(2) click_button 'Last created' page.within '.dropdown-menu-sort' do @@ -352,4 +261,38 @@ describe 'Filter merge requests', feature: true do end end end + + describe 'filter by assignee id', js: true do + it 'filter by current user' do + visit namespace_project_merge_requests_path(project.namespace, project, assignee_id: user.id) + + expect_filtered_search_input("assignee:@#{user.username}") + end + + it 'filter by new user' do + new_user = create(:user) + project.add_developer(new_user) + + visit namespace_project_merge_requests_path(project.namespace, project, assignee_id: new_user.id) + + expect_filtered_search_input("assignee:@#{new_user.username}") + end + end + + describe 'filter by author id', js: true do + it 'filter by current user' do + visit namespace_project_merge_requests_path(project.namespace, project, author_id: user.id) + + expect_filtered_search_input("author:@#{user.username}") + end + + it 'filter by new user' do + new_user = create(:user) + project.add_developer(new_user) + + visit namespace_project_merge_requests_path(project.namespace, project, author_id: new_user.id) + + expect_filtered_search_input("author:@#{new_user.username}") + end + end end diff --git a/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb b/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb index f2f8f11ab28..0ceaf7bc830 100644 --- a/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb +++ b/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb @@ -34,7 +34,7 @@ feature 'Merge immediately', :feature, :js do click_link 'Merge Immediately' - expect(find('.js-merge-button')).to have_content('Merge in progress') + expect(find('.js-merge-when-pipeline-succeeds-button')).to have_content('Merge in progress') end end end diff --git a/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb b/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb index 2ea9c317bd1..ed7193b9777 100644 --- a/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb +++ b/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb @@ -75,7 +75,7 @@ feature 'Merge When Pipeline Succeeds', :feature, :js do context 'when it was enabled and then canceled' do let(:merge_request) do create(:merge_request_with_diffs, - :merge_when_build_succeeds, + :merge_when_pipeline_succeeds, source_project: project, title: 'Bug NS-04', author: user, @@ -97,7 +97,7 @@ feature 'Merge When Pipeline Succeeds', :feature, :js do author: user, merge_user: user, title: 'MepMep', - merge_when_build_succeeds: true) + merge_when_pipeline_succeeds: true) end let!(:build) do diff --git a/spec/features/merge_requests/mini_pipeline_graph_spec.rb b/spec/features/merge_requests/mini_pipeline_graph_spec.rb index b08bd36bde9..84ad8765d8f 100644 --- a/spec/features/merge_requests/mini_pipeline_graph_spec.rb +++ b/spec/features/merge_requests/mini_pipeline_graph_spec.rb @@ -66,7 +66,7 @@ feature 'Mini Pipeline Graph', :js, :feature do end it 'should close when toggle is clicked again' do - toggle.click + toggle.trigger('click') expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu') end diff --git a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb index d2f5c4afc93..447764566e0 100644 --- a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb +++ b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Only allow merge requests to be merged if the build succeeds', feature: true do +feature 'Only allow merge requests to be merged if the pipeline succeeds', feature: true do let(:merge_request) { create(:merge_request_with_diffs) } let(:project) { merge_request.target_project } @@ -27,9 +27,9 @@ feature 'Only allow merge requests to be merged if the build succeeds', feature: status: status) end - context 'when merge requests can only be merged if the build succeeds' do + context 'when merge requests can only be merged if the pipeline succeeds' do before do - project.update_attribute(:only_allow_merge_if_build_succeeds, true) + project.update_attribute(:only_allow_merge_if_pipeline_succeeds, true) end context 'when CI is running' do @@ -88,7 +88,7 @@ feature 'Only allow merge requests to be merged if the build succeeds', feature: context 'when merge requests can be merged when the build failed' do before do - project.update_attribute(:only_allow_merge_if_build_succeeds, false) + project.update_attribute(:only_allow_merge_if_pipeline_succeeds, false) end context 'when CI is running' do diff --git a/spec/features/merge_requests/reset_filters_spec.rb b/spec/features/merge_requests/reset_filters_spec.rb index 3a7ece7e1d6..58f11499e3f 100644 --- a/spec/features/merge_requests/reset_filters_spec.rb +++ b/spec/features/merge_requests/reset_filters_spec.rb @@ -1,17 +1,20 @@ require 'rails_helper' feature 'Issues filter reset button', feature: true, js: true do + include FilteredSearchHelpers + include MergeRequestHelpers 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!(:project) { create(:project, :public) } + let!(:user) { create(:user) } + let!(:milestone) { create(:milestone, project: project) } + let!(:bug) { create(:label, project: project, name: 'bug')} let!(:mr1) { create(:merge_request, title: "Feature", source_project: project, target_project: project, source_branch: "Feature", milestone: milestone, author: user, assignee: user) } let!(:mr2) { create(:merge_request, title: "Bugfix1", source_project: project, target_project: project, source_branch: "Bugfix1") } - let(:merge_request_css) { '.merge-request' } + let(:merge_request_css) { '.merge-request' } + let(:clear_search_css) { '.filtered-search-input-container .clear-search' } before do mr2.labels << bug @@ -50,7 +53,7 @@ feature 'Issues filter reset button', feature: true, js: true do context 'when author filter has been applied' do it 'resets the author filter' do - visit_merge_requests(project, author_id: user.id) + visit_merge_requests(project, author_username: user.username) expect(page).to have_css(merge_request_css, count: 1) reset_filters @@ -60,7 +63,7 @@ feature 'Issues filter reset button', feature: true, js: true do context 'when assignee filter has been applied' do it 'resets the assignee filter' do - visit_merge_requests(project, assignee_id: user.id) + visit_merge_requests(project, assignee_username: user.username) expect(page).to have_css(merge_request_css, count: 1) reset_filters @@ -70,7 +73,7 @@ feature 'Issues filter reset button', feature: true, js: true do context 'when all filters have been applied' do it 'resets all filters' do - visit_merge_requests(project, assignee_id: user.id, author_id: user.id, milestone_title: milestone.title, label_name: bug.name, search: 'Bug') + visit_merge_requests(project, assignee_username: user.username, author_username: user.username, milestone_title: milestone.title, label_name: bug.name, search: 'Bug') expect(page).to have_css(merge_request_css, count: 0) reset_filters @@ -82,15 +85,7 @@ feature 'Issues filter reset button', feature: true, js: true do it 'the reset link should not be visible' do visit_merge_requests(project) expect(page).to have_css(merge_request_css, count: 2) - expect(page).not_to have_css '.reset_filters' + expect(page).not_to have_css(clear_search_css) end end - - def visit_merge_requests(project, opts = {}) - visit namespace_project_merge_requests_path project.namespace, project, opts - end - - def reset_filters - find('.reset-filters').click - end end diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_requests/widget_spec.rb index 957e913bf95..b575aeff0d8 100644 --- a/spec/features/merge_requests/widget_spec.rb +++ b/spec/features/merge_requests/widget_spec.rb @@ -3,8 +3,8 @@ require 'rails_helper' describe 'Merge request', :feature, :js do include WaitForAjax - let(:project) { create(:project) } let(:user) { create(:user) } + let(:project) { create(:project) } let(:merge_request) { create(:merge_request, source_project: project) } before do @@ -31,7 +31,7 @@ describe 'Merge request', :feature, :js do wait_for_ajax - expect(page).to have_selector('.accept_merge_request') + expect(page).to have_selector('.accept-merge-request') end end @@ -51,5 +51,83 @@ describe 'Merge request', :feature, :js do expect(find('.js-environment-link')[:href]).to include(environment.formatted_external_url) end end + + it 'shows green accept merge request button' do + # Wait for the `ci_status` and `merge_check` requests + wait_for_ajax + expect(page).to have_selector('.accept-merge-request.btn-create') + end + end + + context 'view merge request with external CI service' do + before do + create(:service, project: project, + active: true, + type: 'CiService', + category: 'ci') + + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'has danger button while waiting for external CI status' do + # Wait for the `ci_status` and `merge_check` requests + wait_for_ajax + expect(page).to have_selector('.accept-merge-request.btn-danger') + end + end + + context 'view merge request with failed GitLab CI pipelines' do + before do + commit_status = create(:commit_status, project: project, status: 'failed') + pipeline = create(:ci_pipeline, project: project, + sha: merge_request.diff_head_sha, + ref: merge_request.source_branch, + status: 'failed', + statuses: [commit_status]) + create(:ci_build, :pending, pipeline: pipeline) + + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'has danger button when not succeeded' do + # Wait for the `ci_status` and `merge_check` requests + wait_for_ajax + expect(page).to have_selector('.accept-merge-request.btn-danger') + end + end + + context 'view merge request with MWBS button' do + before do + commit_status = create(:commit_status, project: project, status: 'pending') + pipeline = create(:ci_pipeline, project: project, + sha: merge_request.diff_head_sha, + ref: merge_request.source_branch, + status: 'pending', + statuses: [commit_status]) + create(:ci_build, :pending, pipeline: pipeline) + + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'has info button when MWBS button' do + # Wait for the `ci_status` and `merge_check` requests + wait_for_ajax + expect(page).to have_selector('.merge-when-pipeline-succeeds.btn-info') + end + end + + context 'merge error' do + before do + allow_any_instance_of(Repository).to receive(:merge).and_return(false) + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + click_button 'Accept Merge Request' + wait_for_ajax + end + + it 'updates the MR widget' do + page.within('.mr-widget-body') do + expect(page).to have_content('Conflicts detected during merge') + end + end end end diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb index a2e40546588..c3297de709a 100644 --- a/spec/features/milestone_spec.rb +++ b/spec/features/milestone_spec.rb @@ -24,7 +24,7 @@ feature 'Milestone', feature: true do find('input[name="commit"]').click expect(find('.alert-success')).to have_content('Assign some issues to this milestone.') - expect(page).to have_content('Nov 16, 2016 - Dec 16, 2016') + expect(page).to have_content('Nov 16, 2016–Dec 16, 2016') end end diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb index 7a562b5e03d..406d7cf791c 100644 --- a/spec/features/profile_spec.rb +++ b/spec/features/profile_spec.rb @@ -4,7 +4,7 @@ describe 'Profile account page', feature: true do let(:user) { create(:user) } before do - login_as :user + login_as(user) end describe 'when signup is enabled' do @@ -16,7 +16,7 @@ describe 'Profile account page', feature: true do it { expect(page).to have_content('Remove account') } it 'deletes the account' do - expect { click_link 'Delete account' }.to change { User.count }.by(-1) + expect { click_link 'Delete account' }.to change { User.where(id: user.id).count }.by(-1) expect(current_path).to eq(new_user_session_path) end end diff --git a/spec/features/profiles/keys_spec.rb b/spec/features/profiles/keys_spec.rb index eb1050d21c6..2f436f153aa 100644 --- a/spec/features/profiles/keys_spec.rb +++ b/spec/features/profiles/keys_spec.rb @@ -15,7 +15,7 @@ feature 'Profile > SSH Keys', feature: true do scenario 'auto-populates the title', js: true do fill_in('Key', with: attributes_for(:key).fetch(:key)) - expect(find_field('Title').value).to eq 'dummy@gitlab.com' + expect(page).to have_field("Title", with: "dummy@gitlab.com") end scenario 'saves the new key' do diff --git a/spec/features/profiles/preferences_spec.rb b/spec/features/profiles/preferences_spec.rb index a6b841c0210..15c8677fcd3 100644 --- a/spec/features/profiles/preferences_spec.rb +++ b/spec/features/profiles/preferences_spec.rb @@ -8,35 +8,6 @@ describe 'Profile > Preferences', feature: true do visit profile_preferences_path end - describe 'User changes their application theme', js: true do - let(:default) { Gitlab::Themes.default } - let(:theme) { Gitlab::Themes.by_id(5) } - - it 'creates a flash message' do - choose "user_theme_id_#{theme.id}" - - expect_preferences_saved_message - end - - it 'updates their preference' do - choose "user_theme_id_#{theme.id}" - - allowing_for_delay do - visit page.current_path - expect(page).to have_checked_field("user_theme_id_#{theme.id}") - end - end - - it 'reflects the changes immediately' do - expect(page).to have_selector("body.#{default.css_class}") - - choose "user_theme_id_#{theme.id}" - - expect(page).not_to have_selector("body.#{default.css_class}") - expect(page).to have_selector("body.#{theme.css_class}") - end - end - describe 'User changes their syntax highlighting theme', js: true do it 'creates a flash message' do choose 'user_color_scheme_id_5' diff --git a/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb b/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb new file mode 100644 index 00000000000..e05fbb3715c --- /dev/null +++ b/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +feature 'Profile > Notifications > User changes notified_of_own_activity setting', feature: true, js: true do + let(:user) { create(:user) } + + before do + login_as(user) + end + + scenario 'User opts into receiving notifications about their own activity' do + visit profile_notifications_path + + expect(page).not_to have_checked_field('user[notified_of_own_activity]') + + check 'user[notified_of_own_activity]' + + expect(page).to have_content('Notification settings saved') + expect(page).to have_checked_field('user[notified_of_own_activity]') + end + + scenario 'User opts out of receiving notifications about their own activity' do + user.update!(notified_of_own_activity: true) + visit profile_notifications_path + + expect(page).to have_checked_field('user[notified_of_own_activity]') + + uncheck 'user[notified_of_own_activity]' + + expect(page).to have_content('Notification settings saved') + expect(page).not_to have_checked_field('user[notified_of_own_activity]') + end +end diff --git a/spec/features/projects/activity/rss_spec.rb b/spec/features/projects/activity/rss_spec.rb new file mode 100644 index 00000000000..b47c6d431eb --- /dev/null +++ b/spec/features/projects/activity/rss_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +feature 'Project Activity RSS' do + let(:project) { create(:empty_project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } + let(:path) { activity_namespace_project_path(project.namespace, project) } + + before do + create(:issue, project: project) + end + + context 'when signed in' do + before do + user = create(:user) + project.team << [user, :developer] + login_as(user) + visit path + end + + it_behaves_like "it has an RSS button with current_user's private token" + end + + context 'when signed out' do + before do + visit path + end + + it_behaves_like "it has an RSS button without a private token" + end +end diff --git a/spec/features/projects/badges/list_spec.rb b/spec/features/projects/badges/list_spec.rb index 67a4a5d1ab1..ae9db0c0d6e 100644 --- a/spec/features/projects/badges/list_spec.rb +++ b/spec/features/projects/badges/list_spec.rb @@ -14,7 +14,8 @@ feature 'list of badges' do expect(page).to have_content 'build status' expect(page).to have_content 'Markdown' expect(page).to have_content 'HTML' - expect(page).to have_css('.highlight', count: 2) + expect(page).to have_content 'AsciiDoc' + expect(page).to have_css('.highlight', count: 3) expect(page).to have_xpath("//img[@alt='build status']") page.within('.highlight', match: :first) do @@ -28,7 +29,8 @@ feature 'list of badges' do expect(page).to have_content 'coverage report' expect(page).to have_content 'Markdown' expect(page).to have_content 'HTML' - expect(page).to have_css('.highlight', count: 2) + expect(page).to have_content 'AsciiDoc' + expect(page).to have_css('.highlight', count: 3) expect(page).to have_xpath("//img[@alt='coverage report']") page.within('.highlight', match: :first) do diff --git a/spec/features/projects/builds_spec.rb b/spec/features/projects/builds_spec.rb index f7e0115643e..2116721b224 100644 --- a/spec/features/projects/builds_spec.rb +++ b/spec/features/projects/builds_spec.rb @@ -109,6 +109,10 @@ feature 'Builds', :feature do expect(page).to have_content pipeline.git_commit_message expect(page).to have_content pipeline.git_author_name end + + it 'shows active build' do + expect(page).to have_selector('.build-job.active') + end end context "Job from other project" do @@ -271,7 +275,7 @@ feature 'Builds', :feature do let!(:deployment) { create(:deployment, environment: environment, sha: project.commit.id) } let(:build) { create(:ci_build, :success, environment: environment.name, pipeline: pipeline) } - it 'shows a link to lastest deployment' do + it 'shows a link to latest deployment' do visit namespace_project_build_path(project.namespace, project, build) expect(page).to have_link('latest deployment') diff --git a/spec/features/projects/commit/cherry_pick_spec.rb b/spec/features/projects/commit/cherry_pick_spec.rb index 7baf7913424..0b972d2a439 100644 --- a/spec/features/projects/commit/cherry_pick_spec.rb +++ b/spec/features/projects/commit/cherry_pick_spec.rb @@ -60,6 +60,7 @@ describe 'Cherry-pick Commits' do click_button 'Cherry-pick' end expect(page).to have_content('The commit has been successfully cherry-picked. You can now submit a merge request to get this change into the original branch.') + expect(page).to have_content("From cherry-pick-#{master_pickable_commit.short_id} into master") end end diff --git a/spec/features/projects/commit/rss_spec.rb b/spec/features/projects/commit/rss_spec.rb new file mode 100644 index 00000000000..6e0e1916f87 --- /dev/null +++ b/spec/features/projects/commit/rss_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +feature 'Project Commits RSS' do + let(:project) { create(:project, :repository, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } + let(:path) { namespace_project_commits_path(project.namespace, project, :master) } + + context 'when signed in' do + before do + user = create(:user) + project.team << [user, :developer] + login_as(user) + visit path + end + + it_behaves_like "it has an RSS button with current_user's private token" + it_behaves_like "an autodiscoverable RSS feed with current_user's private token" + end + + context 'when signed out' do + before do + visit path + end + + it_behaves_like "it has an RSS button without a private token" + it_behaves_like "an autodiscoverable RSS feed without a private token" + end +end diff --git a/spec/features/projects/developer_views_empty_project_instructions_spec.rb b/spec/features/projects/developer_views_empty_project_instructions_spec.rb index 0c51fe72ca4..2352329d58c 100644 --- a/spec/features/projects/developer_views_empty_project_instructions_spec.rb +++ b/spec/features/projects/developer_views_empty_project_instructions_spec.rb @@ -56,8 +56,14 @@ feature 'Developer views empty project instructions', feature: true do end def expect_instructions_for(protocol) - msg = :"#{protocol.downcase}_url_to_repo" - - expect(page).to have_content("git clone #{project.send(msg)}") + url = + case protocol + when 'ssh' + project.ssh_url_to_repo + when 'http' + project.http_url_to_repo(developer) + end + + expect(page).to have_content("git clone #{url}") end end diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb index 64094af29c0..ccadc936567 100644 --- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb +++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb @@ -6,7 +6,7 @@ feature 'project owner creates a license file', feature: true, js: true do let(:project_master) { create(:user) } let(:project) { create(:project) } background do - project.repository.remove_file(project_master, 'LICENSE', + project.repository.delete_file(project_master, 'LICENSE', message: 'Remove LICENSE', branch_name: 'master') project.team << [project_master, :master] login_as(project_master) @@ -25,7 +25,7 @@ feature 'project owner creates a license file', feature: true, js: true do select_template('MIT License') file_content = first('.file-editor') - expect(file_content).to have_content('The MIT License (MIT)') + expect(file_content).to have_content('MIT License') expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}") fill_in :commit_message, with: 'Add a LICENSE file', visible: true @@ -33,7 +33,7 @@ feature 'project owner creates a license file', feature: true, js: true do expect(current_path).to eq( namespace_project_blob_path(project.namespace, project, 'master/LICENSE')) - expect(page).to have_content('The MIT License (MIT)') + expect(page).to have_content('MIT License') expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}") end @@ -49,7 +49,7 @@ feature 'project owner creates a license file', feature: true, js: true do select_template('MIT License') file_content = first('.file-editor') - expect(file_content).to have_content('The MIT License (MIT)') + expect(file_content).to have_content('MIT License') expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}") fill_in :commit_message, with: 'Add a LICENSE file', visible: true @@ -57,7 +57,7 @@ feature 'project owner creates a license file', feature: true, js: true do expect(current_path).to eq( namespace_project_blob_path(project.namespace, project, 'master/LICENSE')) - expect(page).to have_content('The MIT License (MIT)') + expect(page).to have_content('MIT License') expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}") end diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb index 4453b6d485f..420db962318 100644 --- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb +++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb @@ -24,7 +24,7 @@ feature 'project owner sees a link to create a license file in empty project', f select_template('MIT License') file_content = first('.file-editor') - expect(file_content).to have_content('The MIT License (MIT)') + expect(file_content).to have_content('MIT License') expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}") fill_in :commit_message, with: 'Add a LICENSE file', visible: true @@ -34,7 +34,7 @@ feature 'project owner sees a link to create a license file in empty project', f expect(current_path).to eq( namespace_project_blob_path(project.namespace, project, 'master/LICENSE')) - expect(page).to have_content('The MIT License (MIT)') + expect(page).to have_content('MIT License') expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}") end diff --git a/spec/features/projects/guest_navigation_menu_spec.rb b/spec/features/projects/guest_navigation_menu_spec.rb index 8120a51c515..726469daba4 100644 --- a/spec/features/projects/guest_navigation_menu_spec.rb +++ b/spec/features/projects/guest_navigation_menu_spec.rb @@ -15,13 +15,11 @@ describe "Guest navigation menu" do within(".nav-links") do expect(page).to have_content 'Project' - expect(page).to have_content 'Activity' expect(page).to have_content 'Issues' expect(page).to have_content 'Wiki' expect(page).not_to have_content 'Repository' expect(page).not_to have_content 'Pipelines' - expect(page).not_to have_content 'Graphs' expect(page).not_to have_content 'Merge Requests' end end diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb index 16dddb2a86b..40caf89dd54 100644 --- a/spec/features/projects/import_export/export_file_spec.rb +++ b/spec/features/projects/import_export/export_file_spec.rb @@ -9,7 +9,7 @@ feature 'Import/Export - project export integration test', feature: true, js: tr include ExportFileHelper let(:user) { create(:admin) } - let(:export_path) { "#{Dir::tmpdir}/import_file_spec" } + let(:export_path) { "#{Dir.tmpdir}/import_file_spec" } let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys } let(:sensitive_words) { %w[pass secret token key] } diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb index 3015576f6f8..2d1106ea3e8 100644 --- a/spec/features/projects/import_export/import_file_spec.rb +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -4,7 +4,7 @@ feature 'Import/Export - project import integration test', feature: true, js: tr include Select2Helper let(:file) { File.join(Rails.root, 'spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') } - let(:export_path) { "#{Dir::tmpdir}/import_file_spec" } + let(:export_path) { "#{Dir.tmpdir}/import_file_spec" } background do allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) diff --git a/spec/features/projects/import_export/namespace_export_file_spec.rb b/spec/features/projects/import_export/namespace_export_file_spec.rb index d0bafc6168c..cb399ea55df 100644 --- a/spec/features/projects/import_export/namespace_export_file_spec.rb +++ b/spec/features/projects/import_export/namespace_export_file_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' feature 'Import/Export - Namespace export file cleanup', feature: true, js: true do - let(:export_path) { "#{Dir::tmpdir}/import_file_spec" } + let(:export_path) { "#{Dir.tmpdir}/import_file_spec" } let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys } let(:project) { create(:empty_project) } diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb index e90a033b8c4..62d0aedda48 100644 --- a/spec/features/projects/issuable_templates_spec.rb +++ b/spec/features/projects/issuable_templates_spec.rb @@ -18,20 +18,18 @@ feature 'issuable templates', feature: true, js: true do let(:description_addition) { ' appending to description' } background do - project.repository.commit_file( + project.repository.create_file( user, '.gitlab/issue_templates/bug.md', template_content, message: 'added issue template', - branch_name: 'master', - update: false) - project.repository.commit_file( + branch_name: 'master') + project.repository.create_file( user, '.gitlab/issue_templates/test.md', longtemplate_content, message: 'added issue template', - branch_name: 'master', - update: false) + branch_name: 'master') visit edit_namespace_project_issue_path project.namespace, project, issue fill_in :'issue[title]', with: 'test issue title' end @@ -79,13 +77,12 @@ feature 'issuable templates', feature: true, js: true do let(:issue) { create(:issue, author: user, assignee: user, project: project) } background do - project.repository.commit_file( + project.repository.create_file( user, '.gitlab/issue_templates/bug.md', template_content, message: 'added issue template', - branch_name: 'master', - update: false) + branch_name: 'master') visit edit_namespace_project_issue_path project.namespace, project, issue fill_in :'issue[title]', with: 'test issue title' fill_in :'issue[description]', with: prior_description @@ -104,13 +101,12 @@ feature 'issuable templates', feature: true, js: true do let(:merge_request) { create(:merge_request, :with_diffs, source_project: project) } background do - project.repository.commit_file( + project.repository.create_file( user, '.gitlab/merge_request_templates/feature-proposal.md', template_content, message: 'added merge request template', - branch_name: 'master', - update: false) + branch_name: 'master') visit edit_namespace_project_merge_request_path project.namespace, project, merge_request fill_in :'merge_request[title]', with: 'test merge request title' end @@ -135,13 +131,12 @@ feature 'issuable templates', feature: true, js: true do fork_project.team << [fork_user, :master] create(:forked_project_link, forked_to_project: fork_project, forked_from_project: project) login_as fork_user - project.repository.commit_file( + project.repository.create_file( fork_user, '.gitlab/merge_request_templates/feature-proposal.md', template_content, message: 'added merge request template', - branch_name: 'master', - update: false) + branch_name: 'master') visit edit_namespace_project_merge_request_path project.namespace, project, merge_request fill_in :'merge_request[title]', with: 'test merge request title' end diff --git a/spec/features/projects/issues/rss_spec.rb b/spec/features/projects/issues/rss_spec.rb new file mode 100644 index 00000000000..71429f00095 --- /dev/null +++ b/spec/features/projects/issues/rss_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +feature 'Project Issues RSS' do + let(:project) { create(:empty_project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } + let(:path) { namespace_project_issues_path(project.namespace, project) } + + before do + create(:issue, project: project) + end + + context 'when signed in' do + before do + user = create(:user) + project.team << [user, :developer] + login_as(user) + visit path + end + + it_behaves_like "it has an RSS button with current_user's private token" + it_behaves_like "an autodiscoverable RSS feed with current_user's private token" + end + + context 'when signed out' do + before do + visit path + end + + it_behaves_like "it has an RSS button without a private token" + it_behaves_like "an autodiscoverable RSS feed without a private token" + end +end diff --git a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb index 81b0c991d4f..7414ce21f59 100644 --- a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb +++ b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb @@ -37,7 +37,7 @@ feature 'Issue prioritization', feature: true do page.within('.issues-holder') do issue_titles = all('.issues-list .issue-title-text').map(&:text) - expect(issue_titles).to eq(['issue_4', 'issue_3', 'issue_5', 'issue_2', 'issue_1']) + expect(issue_titles).to eq(%w(issue_4 issue_3 issue_5 issue_2 issue_1)) end end end @@ -77,7 +77,7 @@ feature 'Issue prioritization', feature: true do expect(issue_titles[0..1]).to contain_exactly('issue_5', 'issue_8') expect(issue_titles[2..4]).to contain_exactly('issue_1', 'issue_3', 'issue_7') - expect(issue_titles[5..-1]).to eq(['issue_2', 'issue_4', 'issue_6']) + expect(issue_titles[5..-1]).to eq(%w(issue_2 issue_4 issue_6)) end end end diff --git a/spec/features/projects/main/download_buttons_spec.rb b/spec/features/projects/main/download_buttons_spec.rb index 227ccf9459c..02198ff3e41 100644 --- a/spec/features/projects/main/download_buttons_spec.rb +++ b/spec/features/projects/main/download_buttons_spec.rb @@ -39,6 +39,13 @@ feature 'Download buttons in project main page', feature: true do expect(page).to have_link "Download '#{build.name}'", href: href end + + scenario 'download links have download attribute' do + expect(page).to have_selector('a', text: 'Download') + page.all('a', text: 'Download').each do |link| + expect(link[:download]).to eq '' + end + end end end end diff --git a/spec/features/projects/main/rss_spec.rb b/spec/features/projects/main/rss_spec.rb new file mode 100644 index 00000000000..b1a3af612a1 --- /dev/null +++ b/spec/features/projects/main/rss_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +feature 'Project RSS' do + let(:project) { create(:project, :repository, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } + let(:path) { namespace_project_path(project.namespace, project) } + + context 'when signed in' do + before do + user = create(:user) + project.team << [user, :developer] + login_as(user) + visit path + end + + it_behaves_like "an autodiscoverable RSS feed with current_user's private token" + end + + context 'when signed out' do + before do + visit path + end + + it_behaves_like "an autodiscoverable RSS feed without a private token" + end +end diff --git a/spec/features/projects/members/sorting_spec.rb b/spec/features/projects/members/sorting_spec.rb index d6ebb523f95..c7a32a65e49 100644 --- a/spec/features/projects/members/sorting_spec.rb +++ b/spec/features/projects/members/sorting_spec.rb @@ -85,7 +85,7 @@ feature 'Projects > Members > Sorting', feature: true do end def visit_members_list(sort:) - visit namespace_project_project_members_path(project.namespace.to_param, project.to_param, sort: sort) + visit namespace_project_project_members_path(project.namespace.to_param, project, sort: sort) end def first_member diff --git a/spec/features/projects/milestones/milestone_spec.rb b/spec/features/projects/milestones/milestone_spec.rb new file mode 100644 index 00000000000..df229d0aa78 --- /dev/null +++ b/spec/features/projects/milestones/milestone_spec.rb @@ -0,0 +1,64 @@ +require 'spec_helper' + +feature 'Project milestone', :feature do + let(:user) { create(:user) } + let(:project) { create(:empty_project, name: 'test', namespace: user.namespace) } + let(:milestone) { create(:milestone, project: project) } + + before do + login_as(user) + end + + context 'when project has enabled issues' do + before do + visit namespace_project_milestone_path(project.namespace, project, milestone) + end + + it 'shows issues tab' do + within('#content-body') do + expect(page).to have_link 'Issues', href: '#tab-issues' + expect(page).to have_selector '.nav-links li.active', count: 1 + expect(find('.nav-links li.active')).to have_content 'Issues' + end + end + + it 'shows issues stats' do + expect(page).to have_content 'issues:' + end + + it 'shows Browse Issues button' do + within('#content-body') do + expect(page).to have_link 'Browse Issues' + end + end + end + + context 'when project has disabled issues' do + before do + project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) + visit namespace_project_milestone_path(project.namespace, project, milestone) + end + + it 'hides issues tab' do + within('#content-body') do + expect(page).not_to have_link 'Issues', href: '#tab-issues' + expect(page).to have_selector '.nav-links li.active', count: 1 + expect(find('.nav-links li.active')).to have_content 'Merge Requests' + end + end + + it 'hides issues stats' do + expect(page).to have_no_content 'issues:' + end + + it 'hides Browse Issues button' do + within('#content-body') do + expect(page).not_to have_link 'Browse Issues' + end + end + + it 'does not show an informative message' do + expect(page).not_to have_content('Assign some issues to this milestone.') + end + end +end diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb index b56e562b2b6..45185f2dd1f 100644 --- a/spec/features/projects/new_project_spec.rb +++ b/spec/features/projects/new_project_spec.rb @@ -19,6 +19,51 @@ feature "New project", feature: true do end end + context "Namespace selector" do + context "with user namespace" do + before do + visit new_project_path + end + + it "selects the user namespace" do + namespace = find("#project_namespace_id") + + expect(namespace.text).to eq user.username + end + end + + context "with group namespace" do + let(:group) { create(:group, :private, owner: user) } + + before do + group.add_owner(user) + visit new_project_path(namespace_id: group.id) + end + + it "selects the group namespace" do + namespace = find("#project_namespace_id option[selected]") + + expect(namespace.text).to eq group.name + end + + context "on validation error" do + before do + fill_in('project_path', with: 'private-group-project') + choose('Internal') + click_button('Create project') + + expect(page).to have_css '.project-edit-errors .alert.alert-danger' + end + + it "selects the group namespace" do + namespace = find("#project_namespace_id option[selected]") + + expect(namespace.text).to eq group.name + end + end + end + end + context 'Import project options' do before do visit new_project_path diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 0b5ccc8c515..9f06e52ab55 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -54,7 +54,7 @@ describe 'Pipeline', :feature, :js do expect(page).to have_content('Build') expect(page).to have_content('Test') expect(page).to have_content('Deploy') - expect(page).to have_content('Retry failed') + expect(page).to have_content('Retry') expect(page).to have_content('Cancel running') end @@ -164,9 +164,9 @@ describe 'Pipeline', :feature, :js do it { expect(page).not_to have_content('retried') } context 'when retrying' do - before { click_on 'Retry failed' } + before { find('.js-retry-button').trigger('click') } - it { expect(page).not_to have_content('Retry failed') } + it { expect(page).not_to have_content('Retry') } end end @@ -198,7 +198,7 @@ describe 'Pipeline', :feature, :js do expect(page).to have_content(build_failed.id) expect(page).to have_content(build_running.id) expect(page).to have_content(build_external.id) - expect(page).to have_content('Retry failed') + expect(page).to have_content('Retry') expect(page).to have_content('Cancel running') expect(page).to have_link('Play') end @@ -226,9 +226,9 @@ describe 'Pipeline', :feature, :js do it { expect(page).not_to have_content('retried') } context 'when retrying' do - before { click_on 'Retry failed' } + before { find('.js-retry-button').trigger('click') } - it { expect(page).not_to have_content('Retry failed') } + it { expect(page).not_to have_content('Retry') } it { expect(page).to have_selector('.retried') } end end diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 6555b2fc6c1..22bf1bfbdf0 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -26,22 +26,66 @@ describe 'Pipelines', :feature, :js do ) end - [:all, :running, :branches].each do |scope| - context "when displaying #{scope}" do - before do - visit_project_pipelines(scope: scope) - end + context 'scope' do + before do + create(:ci_empty_pipeline, status: 'pending', project: project, sha: project.commit.id, ref: 'master') + create(:ci_empty_pipeline, status: 'running', project: project, sha: project.commit.id, ref: 'master') + create(:ci_empty_pipeline, status: 'created', project: project, sha: project.commit.id, ref: 'master') + create(:ci_empty_pipeline, status: 'success', project: project, sha: project.commit.id, ref: 'master') + end - it 'contains pipeline commit short SHA' do - expect(page).to have_content(pipeline.short_sha) - end + [:all, :running, :pending, :finished, :branches].each do |scope| + context "when displaying #{scope}" do + before do + visit_project_pipelines(scope: scope) + end + + it 'contains pipeline commit short SHA' do + expect(page).to have_content(pipeline.short_sha) + end - it 'contains branch name' do - expect(page).to have_content(pipeline.ref) + it 'contains branch name' do + expect(page).to have_content(pipeline.ref) + end end end end + context 'header tabs' do + before do + visit namespace_project_pipelines_path(project.namespace, project) + wait_for_vue_resource + end + + it 'shows a tab for All pipelines and count' do + expect(page.find('.js-pipelines-tab-all a').text).to include('All') + expect(page.find('.js-pipelines-tab-all .badge').text).to include('1') + end + + it 'shows a tab for Pending pipelines and count' do + expect(page.find('.js-pipelines-tab-pending a').text).to include('Pending') + expect(page.find('.js-pipelines-tab-pending .badge').text).to include('0') + end + + it 'shows a tab for Running pipelines and count' do + expect(page.find('.js-pipelines-tab-running a').text).to include('Running') + expect(page.find('.js-pipelines-tab-running .badge').text).to include('1') + end + + it 'shows a tab for Finished pipelines and count' do + expect(page.find('.js-pipelines-tab-finished a').text).to include('Finished') + expect(page.find('.js-pipelines-tab-finished .badge').text).to include('0') + end + + it 'shows a tab for Branches' do + expect(page.find('.js-pipelines-tab-branches a').text).to include('Branches') + end + + it 'shows a tab for Tags' do + expect(page.find('.js-pipelines-tab-tags a').text).to include('Tags') + end + end + context 'when pipeline is cancelable' do let!(:build) do create(:ci_build, pipeline: pipeline, @@ -218,6 +262,14 @@ describe 'Pipelines', :feature, :js do expect(page).to have_link(with_artifacts.name) end + + it 'has download attribute on download links' do + find('.js-pipeline-dropdown-download').click + expect(page).to have_selector('a', text: 'Download') + page.all('.build-artifacts a', text: 'Download').each do |link| + expect(link[:download]).to eq '' + end + end end context 'with artifacts expired' do @@ -276,6 +328,39 @@ describe 'Pipelines', :feature, :js do expect(build.reload).to be_canceled end end + + context 'dropdown jobs list' do + it 'should keep the dropdown open when the user ctr/cmd + clicks in the job name' do + find('.js-builds-dropdown-button').trigger('click') + + execute_script('var e = $.Event("keydown", { keyCode: 64 }); $("body").trigger(e);') + + find('.mini-pipeline-graph-dropdown-item').trigger('click') + + expect(page).to have_selector('.js-ci-action-icon') + end + end + end + + context 'with pagination' do + before do + allow(Ci::Pipeline).to receive(:default_per_page).and_return(1) + create(:ci_empty_pipeline, project: project) + end + + it 'should render pagination' do + visit namespace_project_pipelines_path(project.namespace, project) + wait_for_vue_resource + + expect(page).to have_selector('.gl-pagination') + end + + it 'should render second page of pipelines' do + visit namespace_project_pipelines_path(project.namespace, project, page: '2') + wait_for_vue_resource + + expect(page).to have_selector('.gl-pagination .page', count: 2) + end end end @@ -286,8 +371,14 @@ describe 'Pipelines', :feature, :js do visit new_namespace_project_pipeline_path(project.namespace, project) end - context 'for valid commit' do - before { fill_in('pipeline[ref]', with: 'master') } + context 'for valid commit', js: true do + before do + click_button project.default_branch + + page.within '.dropdown-menu' do + click_link 'master' + end + end context 'with gitlab-ci.yml' do before { stub_ci_pipeline_to_return_yaml_file } @@ -304,15 +395,6 @@ describe 'Pipelines', :feature, :js do it { expect(page).to have_content('Missing .gitlab-ci.yml file') } end end - - context 'for invalid commit' do - before do - fill_in('pipeline[ref]', with: 'invalid-reference') - click_on 'Create pipeline' - end - - it { expect(page).to have_content('Reference not found') } - end end describe 'Create pipelines' do @@ -324,18 +406,22 @@ describe 'Pipelines', :feature, :js do describe 'new pipeline page' do it 'has field to add a new pipeline' do - expect(page).to have_field('pipeline[ref]') + expect(page).to have_selector('.js-branch-select') + expect(find('.js-branch-select')).to have_content project.default_branch expect(page).to have_content('Create for') end end describe 'find pipelines' do it 'shows filtered pipelines', js: true do - fill_in('pipeline[ref]', with: 'fix') - find('input#ref').native.send_keys(:keydown) + click_button project.default_branch + + page.within '.dropdown-menu' do + find('.dropdown-input-field').native.send_keys('fix') - within('.ui-autocomplete') do - expect(page).to have_selector('li', text: 'fix') + page.within '.dropdown-content' do + expect(page).to have_content('fix') + end end end end diff --git a/spec/features/projects/ref_switcher_spec.rb b/spec/features/projects/ref_switcher_spec.rb index 38fe2d92885..3b8f0b2d3f8 100644 --- a/spec/features/projects/ref_switcher_spec.rb +++ b/spec/features/projects/ref_switcher_spec.rb @@ -20,9 +20,11 @@ feature 'Ref switcher', feature: true, js: true do input.set 'binary' wait_for_ajax - input.native.send_keys :down - input.native.send_keys :down - input.native.send_keys :enter + expect(find('.dropdown-content ul')).to have_selector('li', count: 6) + + page.within '.dropdown-content ul' do + input.native.send_keys :enter + end end expect(page).to have_title 'binary-encoding' diff --git a/spec/features/projects/tree/rss_spec.rb b/spec/features/projects/tree/rss_spec.rb new file mode 100644 index 00000000000..9ac51997d65 --- /dev/null +++ b/spec/features/projects/tree/rss_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +feature 'Project Tree RSS' do + let(:project) { create(:project, :repository, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } + let(:path) { namespace_project_tree_path(project.namespace, project, :master) } + + context 'when signed in' do + before do + user = create(:user) + project.team << [user, :developer] + login_as(user) + visit path + end + + it_behaves_like "an autodiscoverable RSS feed with current_user's private token" + end + + context 'when signed out' do + before do + visit path + end + + it_behaves_like "an autodiscoverable RSS feed without a private token" + end +end diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb index 0fe5a897565..7da05defa81 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -186,7 +186,7 @@ describe "Search", feature: true do sleep 2 expect(page).to have_selector('.merge-requests-holder') - expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + expect(find('.filtered-search').value).to eq("assignee:@#{user.username}") end it 'takes user to her MR page when MR authored is clicked' do @@ -194,7 +194,7 @@ describe "Search", feature: true do sleep 2 expect(page).to have_selector('.merge-requests-holder') - expect(find('.js-author-search .dropdown-toggle-text')).to have_content(user.name) + expect(find('.filtered-search').value).to eq("author:@#{user.username}") end end diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb index 1b352be9331..3495091a0d5 100644 --- a/spec/features/todos/todos_spec.rb +++ b/spec/features/todos/todos_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe 'Dashboard Todos', feature: true do + include WaitForAjax + let(:user) { create(:user) } let(:author) { create(:user) } let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } @@ -34,39 +36,64 @@ describe 'Dashboard Todos', feature: true do end end - describe 'deleting the todo' do + shared_examples 'deleting the todo' do before do - first('.done-todo').click + first('.js-done-todo').click + end + + it 'is marked as done-reversible in the list' do + expect(page).to have_selector('.todos-list .todo.todo-pending.done-reversible') + end + + it 'shows Undo button' do + expect(page).to have_selector('.js-undo-todo', visible: true) + expect(page).to have_selector('.js-done-todo', visible: false) end - it 'is removed from the list' do - expect(page).not_to have_selector('.todos-list .todo') + it 'updates todo count' do + expect(page).to have_content 'To do 0' + expect(page).to have_content 'Done 1' end - it 'shows "All done" message' do - expect(page).to have_selector('.todos-all-done', count: 1) + it 'has not "All done" message' do + expect(page).not_to have_selector('.todos-all-done') end end - context 'todo is stale on the page' do + shared_examples 'deleting and restoring the todo' do before do - todos = TodosFinder.new(user, state: :pending).execute - TodoService.new.mark_todos_as_done(todos, user) + first('.js-done-todo').click + wait_for_ajax + first('.js-undo-todo').click + end + + it 'is marked back as pending in the list' do + expect(page).not_to have_selector('.todos-list .todo.todo-pending.done-reversible') + expect(page).to have_selector('.todos-list .todo.todo-pending') + end + + it 'shows Done button' do + expect(page).to have_selector('.js-undo-todo', visible: false) + expect(page).to have_selector('.js-done-todo', visible: true) end - describe 'deleting the todo' do - before do - first('.done-todo').click - end + it 'updates todo count' do + expect(page).to have_content 'To do 1' + expect(page).to have_content 'Done 0' + end + end - it 'is removed from the list' do - expect(page).not_to have_selector('.todos-list .todo') - end + it_behaves_like 'deleting the todo' + it_behaves_like 'deleting and restoring the todo' - it 'shows "All done" message' do - expect(page).to have_selector('.todos-all-done', count: 1) - 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 + + it_behaves_like 'deleting the todo' + it_behaves_like 'deleting and restoring the todo' end end @@ -113,18 +140,6 @@ describe 'Dashboard Todos', feature: true do expect(page).to have_selector('.gl-pagination .page', count: 2) end - describe 'completing last todo from last page', js: true do - it 'redirects to the previous page' do - visit dashboard_todos_path(page: 2) - expect(page).to have_css("#todo_#{Todo.last.id}") - - click_link('Done') - - expect(current_path).to eq dashboard_todos_path - 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 @@ -156,6 +171,29 @@ describe 'Dashboard Todos', feature: true do end end + context 'User have large number of todos' do + before do + create_list(:todo, 101, :mentioned, user: user, project: project, target: issue, author: author) + + login_as(user) + visit dashboard_todos_path + end + + it 'shows 99+ for count >= 100 in notification' do + expect(page).to have_selector('.todos-pending-count', text: '99+') + end + + it 'shows exact number in To do tab' do + expect(page).to have_selector('.todos-pending .badge', text: '101') + end + + it 'shows exact number for count < 100' do + 3.times { first('.js-done-todo').click } + + expect(page).to have_selector('.todos-pending-count', text: '98') + end + end + context 'User has a Build Failed todo' do let!(:todo) { create(:todo, :build_failed, user: user, project: project, author: author) } diff --git a/spec/features/uploads/user_uploads_avatar_to_group_spec.rb b/spec/features/uploads/user_uploads_avatar_to_group_spec.rb new file mode 100644 index 00000000000..f88a515f7fc --- /dev/null +++ b/spec/features/uploads/user_uploads_avatar_to_group_spec.rb @@ -0,0 +1,26 @@ +require 'rails_helper' + +feature 'User uploads avatar to group', feature: true do + scenario 'they see the new avatar' do + user = create(:user) + group = create(:group) + group.add_owner(user) + login_as(user) + + visit edit_group_path(group) + attach_file( + 'group_avatar', + Rails.root.join('spec', 'fixtures', 'dk.png'), + visible: false + ) + + click_button 'Save group' + + visit group_path(group) + + expect(page).to have_selector(%Q(img[src$="/uploads/group/avatar/#{group.id}/dk.png"])) + + # Cheating here to verify something that isn't user-facing, but is important + expect(group.reload.avatar.file).to exist + end +end diff --git a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb new file mode 100644 index 00000000000..0dfd29045e5 --- /dev/null +++ b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb @@ -0,0 +1,24 @@ +require 'rails_helper' + +feature 'User uploads avatar to profile', feature: true do + scenario 'they see their new avatar' do + user = create(:user) + login_as(user) + + visit profile_path + attach_file( + 'user_avatar', + Rails.root.join('spec', 'fixtures', 'dk.png'), + visible: false + ) + + click_button 'Update profile settings' + + visit user_path(user) + + expect(page).to have_selector(%Q(img[src$="/uploads/user/avatar/#{user.id}/dk.png"])) + + # Cheating here to verify something that isn't user-facing, but is important + expect(user.reload.avatar.file).to exist + end +end diff --git a/spec/features/uploads/user_uploads_file_to_note_spec.rb b/spec/features/uploads/user_uploads_file_to_note_spec.rb new file mode 100644 index 00000000000..0c160dd74b4 --- /dev/null +++ b/spec/features/uploads/user_uploads_file_to_note_spec.rb @@ -0,0 +1,22 @@ +require 'rails_helper' + +feature 'User uploads file to note', feature: true do + include DropzoneHelper + + let(:user) { create(:user) } + let(:project) { create(:empty_project, creator: user, namespace: user.namespace) } + + scenario 'they see the attached file', js: true do + issue = create(:issue, project: project, author: user) + + login_as(user) + visit namespace_project_issue_path(project.namespace, project, issue) + + dropzone_file(Rails.root.join('spec', 'fixtures', 'dk.png')) + click_button 'Comment' + wait_for_ajax + + expect(find('a.no-attachment-icon img[alt="dk"]')['src']) + .to match(%r{/#{project.full_path}/uploads/\h{32}/dk\.png$}) + end +end diff --git a/spec/features/user_callout_spec.rb b/spec/features/user_callout_spec.rb new file mode 100644 index 00000000000..336c4092c98 --- /dev/null +++ b/spec/features/user_callout_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe 'User Callouts', js: true do + let(:user) { create(:user) } + let(:project) { create(:empty_project, path: 'gitlab', name: 'sample') } + + before do + login_as(user) + project.team << [user, :master] + end + + it 'takes you to the profile preferences when the link is clicked' do + visit dashboard_projects_path + click_link 'Check it out' + expect(current_path).to eq profile_preferences_path + end + + describe 'user callout should appear in two routes' do + it 'shows up on the user profile' do + visit user_path(user) + expect(find('.user-callout')).to have_content 'Customize your experience' + end + + it 'shows up on the dashboard projects' do + visit dashboard_projects_path + expect(find('.user-callout')).to have_content 'Customize your experience' + end + end + + it 'hides the user callout when click on the dismiss icon' do + visit user_path(user) + within('.user-callout') do + find('.close-user-callout').click + end + expect(page).not_to have_selector('#user-callout') + end +end diff --git a/spec/features/users/rss_spec.rb b/spec/features/users/rss_spec.rb new file mode 100644 index 00000000000..14564abb16d --- /dev/null +++ b/spec/features/users/rss_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +feature 'User RSS' do + let(:path) { user_path(create(:user)) } + + context 'when signed in' do + before do + login_as(create(:user)) + visit path + end + + it_behaves_like "it has an RSS button with current_user's private token" + end + + context 'when signed out' do + before do + visit path + end + + it_behaves_like "it has an RSS button without a private token" + end +end diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb index 9a4bc027004..a362d6fd3b6 100644 --- a/spec/features/variables_spec.rb +++ b/spec/features/variables_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe 'Project variables', js: true do let(:user) { create(:user) } let(:project) { create(:project) } - let(:variable) { create(:ci_variable, key: 'test') } + let(:variable) { create(:ci_variable, key: 'test_key', value: 'test value') } before do login_as(user) @@ -24,11 +24,23 @@ describe 'Project variables', js: true do fill_in('variable_value', with: 'key value') click_button('Add new variable') + expect(page).to have_content('Variables were successfully updated.') page.within('.variables-table') do expect(page).to have_content('key') end end + it 'adds empty variable' do + fill_in('variable_key', with: 'new_key') + fill_in('variable_value', with: '') + click_button('Add new variable') + + expect(page).to have_content('Variables were successfully updated.') + page.within('.variables-table') do + expect(page).to have_content('new_key') + end + end + it 'reveals and hides new variable' do fill_in('variable_key', with: 'key') fill_in('variable_value', with: 'key value') @@ -72,8 +84,20 @@ describe 'Project variables', js: true do fill_in('variable_value', with: 'key value') click_button('Save variable') + expect(page).to have_content('Variable was successfully updated.') + expect(project.variables.first.value).to eq('key value') + end + + it 'edits variable with empty value' do page.within('.variables-table') do - expect(page).to have_content('key') + find('.btn-variable-edit').click end + + expect(page).to have_content('Update variable') + fill_in('variable_value', with: '') + click_button('Save variable') + + expect(page).to have_content('Variable was successfully updated.') + expect(project.variables.first.value).to eq('') end end diff --git a/spec/finders/group_members_finder_spec.rb b/spec/finders/group_members_finder_spec.rb new file mode 100644 index 00000000000..b762756f9ce --- /dev/null +++ b/spec/finders/group_members_finder_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe GroupMembersFinder, '#execute' do + let(:group) { create(:group) } + let(:nested_group) { create(:group, :access_requestable, parent: group) } + let(:user1) { create(:user) } + let(:user2) { create(:user) } + let(:user3) { create(:user) } + let(:user4) { create(:user) } + + it 'returns members for top-level group' do + member1 = group.add_master(user1) + member2 = group.add_master(user2) + member3 = group.add_master(user3) + + result = described_class.new(group).execute + + expect(result.to_a).to eq([member3, member2, member1]) + end + + it 'returns members for nested group' do + group.add_master(user2) + nested_group.request_access(user4) + member1 = group.add_master(user1) + member3 = nested_group.add_master(user2) + member4 = nested_group.add_master(user3) + + result = described_class.new(nested_group).execute + + expect(result.to_a).to eq([member4, member3, member1]) + end +end diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 12ab1d6dde8..2a008427478 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -136,10 +136,10 @@ describe IssuesFinder do end end - context 'filtering by issue iid' do - let(:params) { { search: issue3.to_reference } } + context 'filtering by issues iids' do + let(:params) { { iids: issue3.iid } } - it 'returns issue with iid match' do + it 'returns issues with iids match' do expect(issues).to contain_exactly(issue3) end end diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index 3dcd7781e5b..21ef94ac5d1 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -38,5 +38,13 @@ describe MergeRequestsFinder do merge_requests = MergeRequestsFinder.new(user, params).execute expect(merge_requests.size).to eq(3) end + + it 'filters by iid' do + params = { project_id: project1.id, iids: merge_request1.iid } + + merge_requests = MergeRequestsFinder.new(user, params).execute + + expect(merge_requests).to contain_exactly(merge_request1) + end end end diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb index f8b05d4e9bc..77a04507be1 100644 --- a/spec/finders/notes_finder_spec.rb +++ b/spec/finders/notes_finder_spec.rb @@ -111,7 +111,7 @@ describe NotesFinder do end it 'raises an exception for an invalid target_type' do - params.merge!(target_type: 'invalid') + params[:target_type] = 'invalid' expect { described_class.new(project, user, params).execute }.to raise_error('invalid target_type') end diff --git a/spec/finders/pipelines_finder_spec.rb b/spec/finders/pipelines_finder_spec.rb index fdc8215aa47..6bada7b3eb9 100644 --- a/spec/finders/pipelines_finder_spec.rb +++ b/spec/finders/pipelines_finder_spec.rb @@ -39,8 +39,8 @@ describe PipelinesFinder do end end - # Scoping to running will speed up the test as it doesn't hit the FS - let(:params) { { scope: 'running' } } + # Scoping to pending will speed up the test as it doesn't hit the FS + let(:params) { { scope: 'pending' } } it 'orders in descending order on ID' do feature_pipeline = create(:ci_pipeline, project: project, ref: 'feature') diff --git a/spec/fixtures/api/schemas/user/login.json b/spec/fixtures/api/schemas/user/login.json index e6c1d9c9d84..6181b3ccc86 100644 --- a/spec/fixtures/api/schemas/user/login.json +++ b/spec/fixtures/api/schemas/user/login.json @@ -19,7 +19,6 @@ "organization", "last_sign_in_at", "confirmed_at", - "theme_id", "color_scheme_id", "projects_limit", "current_sign_in_at", diff --git a/spec/fixtures/api/schemas/user/public.json b/spec/fixtures/api/schemas/user/public.json index dbd5d32e89c..5587cfec61a 100644 --- a/spec/fixtures/api/schemas/user/public.json +++ b/spec/fixtures/api/schemas/user/public.json @@ -19,7 +19,6 @@ "organization", "last_sign_in_at", "confirmed_at", - "theme_id", "color_scheme_id", "projects_limit", "current_sign_in_at", @@ -32,14 +31,14 @@ "properties": { "id": { "type": "integer" }, "username": { "type": "string" }, - "email": { + "email": { "type": "string", "pattern": "^[^@]+@[^@]+$" }, "name": { "type": "string" }, - "state": { + "state": { "type": "string", - "enum": ["active", "blocked"] + "enum": ["active", "blocked"] }, "avatar_url": { "type": "string" }, "web_url": { "type": "string" }, @@ -54,18 +53,17 @@ "organization": { "type": ["string", "null"] }, "last_sign_in_at": { "type": "date" }, "confirmed_at": { "type": ["date", "null"] }, - "theme_id": { "type": "integer" }, "color_scheme_id": { "type": "integer" }, "projects_limit": { "type": "integer" }, "current_sign_in_at": { "type": "date" }, - "identities": { + "identities": { "type": "array", "items": { "type": "object", "properties": { - "provider": { + "provider": { "type": "string", - "enum": ["github", "bitbucket", "google_oauth2"] + "enum": ["github", "bitbucket", "google_oauth2"] }, "extern_uid": { "type": ["number", "string"] } } @@ -74,6 +72,6 @@ "can_create_group": { "type": "boolean" }, "can_create_project": { "type": "boolean" }, "two_factor_enabled": { "type": "boolean" }, - "external": { "type": "boolean" } + "external": { "type": "boolean" } } } diff --git a/spec/fixtures/mail_room_disabled.yml b/spec/fixtures/config/mail_room_disabled.yml index 97f8cff051f..97f8cff051f 100644 --- a/spec/fixtures/mail_room_disabled.yml +++ b/spec/fixtures/config/mail_room_disabled.yml diff --git a/spec/fixtures/mail_room_enabled.yml b/spec/fixtures/config/mail_room_enabled.yml index 9c94649244d..9c94649244d 100644 --- a/spec/fixtures/mail_room_enabled.yml +++ b/spec/fixtures/config/mail_room_enabled.yml diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 9749936a2a0..5c07ea8a872 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -58,7 +58,7 @@ describe ApplicationHelper do project = create(:empty_project, avatar: File.open(uploaded_image_temp_path)) avatar_url = "http://#{Gitlab.config.gitlab.host}/uploads/project/avatar/#{project.id}/banana_sample.gif" - expect(helper.project_icon("#{project.namespace.to_param}/#{project.to_param}").to_s). + expect(helper.project_icon(project.full_path).to_s). to eq "<img src=\"#{avatar_url}\" alt=\"Banana sample\" />" end @@ -68,7 +68,7 @@ describe ApplicationHelper do allow_any_instance_of(Project).to receive(:avatar_in_git).and_return(true) avatar_url = "http://#{Gitlab.config.gitlab.host}#{namespace_project_avatar_path(project.namespace, project)}" - expect(helper.project_icon("#{project.namespace.to_param}/#{project.to_param}").to_s).to match( + expect(helper.project_icon(project.full_path).to_s).to match( image_tag(avatar_url)) end end @@ -265,4 +265,9 @@ describe ApplicationHelper do expect(helper.render_markup('foo.adoc', content)).to eq('NOEL') end end + + describe '#active_when' do + it { expect(helper.active_when(true)).to eq('active') } + it { expect(helper.active_when(false)).to eq(nil) } + end end diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb index 49ea4fa6d3e..cd3281d6f51 100644 --- a/spec/helpers/auth_helper_spec.rb +++ b/spec/helpers/auth_helper_spec.rb @@ -55,7 +55,7 @@ describe AuthHelper do context 'all the button based providers are disabled via application_setting' do it 'returns false' do stub_application_setting( - disabled_oauth_sign_in_sources: ['github', 'twitter'] + disabled_oauth_sign_in_sources: %w(github twitter) ) expect(helper.button_based_providers_enabled?).to be false diff --git a/spec/helpers/emails_helper_spec.rb b/spec/helpers/emails_helper_spec.rb index 3223556e1d3..cd112dbb2fb 100644 --- a/spec/helpers/emails_helper_spec.rb +++ b/spec/helpers/emails_helper_spec.rb @@ -43,4 +43,36 @@ describe EmailsHelper do end end end + + describe '#header_logo' do + context 'there is a brand item with a logo' do + it 'returns the brand header logo' do + appearance = create :appearance, header_logo: fixture_file_upload( + Rails.root.join('spec/fixtures/dk.png') + ) + + expect(header_logo).to eq( + %{<img style="height: 50px" src="/uploads/appearance/header_logo/#{appearance.id}/dk.png" alt="Dk" />} + ) + end + end + + context 'there is a brand item without a logo' do + it 'returns the default header logo' do + create :appearance, header_logo: nil + + expect(header_logo).to eq( + %{<img alt="GitLab" src="/images/mailers/gitlab_header_logo.gif" width="55" height="50" />} + ) + end + end + + context 'there is no brand item' do + it 'returns the default header logo' do + expect(header_logo).to eq( + %{<img alt="GitLab" src="/images/mailers/gitlab_header_logo.gif" width="55" height="50" />} + ) + end + end + end end diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb index df71680e44c..93bb711f29a 100644 --- a/spec/helpers/issuables_helper_spec.rb +++ b/spec/helpers/issuables_helper_spec.rb @@ -51,7 +51,7 @@ describe IssuablesHelper do utf8: '✓', author_id: '11', assignee_id: '18', - label_name: ['bug', 'discussion', 'documentation'], + label_name: %w(bug discussion documentation), milestone_title: 'v4.0', sort: 'due_date_asc', namespace_id: 'gitlab-org', diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb index 13fb9c1f1a7..88d853935c7 100644 --- a/spec/helpers/issues_helper_spec.rb +++ b/spec/helpers/issues_helper_spec.rb @@ -55,8 +55,8 @@ describe IssuesHelper do describe "merge_requests_sentence" do subject { merge_requests_sentence(merge_requests)} let(:merge_requests) do - [ build(:merge_request, iid: 1), build(:merge_request, iid: 2), - build(:merge_request, iid: 3)] + [build(:merge_request, iid: 1), build(:merge_request, iid: 2), + build(:merge_request, iid: 3)] end it { is_expected.to eq("!1, !2, or !3") } @@ -113,7 +113,7 @@ describe IssuesHelper do describe "awards_sort" do it "sorts a hash so thumbsup and thumbsdown are always on top" do data = { "thumbsdown" => "some value", "lifter" => "some value", "thumbsup" => "some value" } - expect(awards_sort(data).keys).to eq(["thumbsup", "thumbsdown", "lifter"]) + expect(awards_sort(data).keys).to eq(%w(thumbsup thumbsdown lifter)) end end diff --git a/spec/helpers/milestones_helper_spec.rb b/spec/helpers/milestones_helper_spec.rb index 14a95479339..68b20a1e4fc 100644 --- a/spec/helpers/milestones_helper_spec.rb +++ b/spec/helpers/milestones_helper_spec.rb @@ -17,7 +17,7 @@ describe MilestonesHelper do it { expect(result_for(due_date: yesterday)).to eq("expired on #{yesterday_formatted}") } it { expect(result_for(start_date: tomorrow)).to eq("starts on #{tomorrow_formatted}") } it { expect(result_for(start_date: yesterday)).to eq("started on #{yesterday_formatted}") } - it { expect(result_for(start_date: yesterday, due_date: tomorrow)).to eq("#{yesterday_formatted} - #{tomorrow_formatted}") } + it { expect(result_for(start_date: yesterday, due_date: tomorrow)).to eq("#{yesterday_formatted}–#{tomorrow_formatted}") } end describe '#milestone_counts' do diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb index dc07657e101..2cc0b40b2d0 100644 --- a/spec/helpers/page_layout_helper_spec.rb +++ b/spec/helpers/page_layout_helper_spec.rb @@ -40,6 +40,18 @@ describe PageLayoutHelper do end end + describe 'favicon' do + it 'defaults to favicon.ico' do + allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('production')) + expect(helper.favicon).to eq 'favicon.ico' + end + + it 'has blue favicon for development' do + allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('development')) + expect(helper.favicon).to eq 'favicon-blue.ico' + end + end + describe 'page_image' do it 'defaults to the GitLab logo' do expect(helper.page_image).to end_with 'assets/gitlab_logo.png' diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb index 1f02e06e312..f3e79cc7290 100644 --- a/spec/helpers/preferences_helper_spec.rb +++ b/spec/helpers/preferences_helper_spec.rb @@ -26,32 +26,6 @@ describe PreferencesHelper do end end - describe 'user_application_theme' do - context 'with a user' do - it "returns user's theme's css_class" do - stub_user(theme_id: 3) - - expect(helper.user_application_theme).to eq 'ui_green' - end - - it 'returns the default when id is invalid' do - stub_user(theme_id: Gitlab::Themes.count + 5) - - allow(Gitlab.config.gitlab).to receive(:default_theme).and_return(2) - - expect(helper.user_application_theme).to eq 'ui_charcoal' - end - end - - context 'without a user' do - it 'returns the default theme' do - stub_user - - expect(helper.user_application_theme).to eq Gitlab::Themes.default.css_class - end - end - end - describe 'user_color_scheme' do context 'with a user' do it "returns user's scheme's css_class" do diff --git a/spec/helpers/rss_helper_spec.rb b/spec/helpers/rss_helper_spec.rb new file mode 100644 index 00000000000..f3f174f3d14 --- /dev/null +++ b/spec/helpers/rss_helper_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe RssHelper do + describe '#rss_url_options' do + context 'when signed in' do + it "includes the current_user's private_token" do + current_user = create(:user) + allow(helper).to receive(:current_user).and_return(current_user) + expect(helper.rss_url_options).to include private_token: current_user.private_token + end + end + + context 'when signed out' do + it "does not have a private_token" do + allow(helper).to receive(:current_user).and_return(nil) + expect(helper.rss_url_options[:private_token]).to be_nil + end + end + end +end diff --git a/spec/helpers/submodule_helper_spec.rb b/spec/helpers/submodule_helper_spec.rb index 4da1569e59f..28b8def331d 100644 --- a/spec/helpers/submodule_helper_spec.rb +++ b/spec/helpers/submodule_helper_spec.rb @@ -20,97 +20,97 @@ describe SubmoduleHelper do it 'detects ssh on standard port' do allow(Gitlab.config.gitlab_shell).to receive(:ssh_port).and_return(22) # set this just to be sure allow(Gitlab.config.gitlab_shell).to receive(:ssh_path_prefix).and_return(Settings.send(:build_gitlab_shell_ssh_path_prefix)) - stub_url([ config.user, '@', config.host, ':gitlab-org/gitlab-ce.git' ].join('')) - expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ]) + stub_url([config.user, '@', config.host, ':gitlab-org/gitlab-ce.git'].join('')) + expect(submodule_links(submodule_item)).to eq([namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash')]) end it 'detects ssh on non-standard port' do allow(Gitlab.config.gitlab_shell).to receive(:ssh_port).and_return(2222) allow(Gitlab.config.gitlab_shell).to receive(:ssh_path_prefix).and_return(Settings.send(:build_gitlab_shell_ssh_path_prefix)) - stub_url([ 'ssh://', config.user, '@', config.host, ':2222/gitlab-org/gitlab-ce.git' ].join('')) - expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ]) + stub_url(['ssh://', config.user, '@', config.host, ':2222/gitlab-org/gitlab-ce.git'].join('')) + expect(submodule_links(submodule_item)).to eq([namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash')]) end it 'detects http on standard port' do allow(Gitlab.config.gitlab).to receive(:port).and_return(80) allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url)) - stub_url([ 'http://', config.host, '/gitlab-org/gitlab-ce.git' ].join('')) - expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ]) + stub_url(['http://', config.host, '/gitlab-org/gitlab-ce.git'].join('')) + expect(submodule_links(submodule_item)).to eq([namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash')]) end it 'detects http on non-standard port' do allow(Gitlab.config.gitlab).to receive(:port).and_return(3000) allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url)) - stub_url([ 'http://', config.host, ':3000/gitlab-org/gitlab-ce.git' ].join('')) - expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ]) + stub_url(['http://', config.host, ':3000/gitlab-org/gitlab-ce.git'].join('')) + expect(submodule_links(submodule_item)).to eq([namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash')]) end it 'works with relative_url_root' do allow(Gitlab.config.gitlab).to receive(:port).and_return(80) # set this just to be sure allow(Gitlab.config.gitlab).to receive(:relative_url_root).and_return('/gitlab/root') allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url)) - stub_url([ 'http://', config.host, '/gitlab/root/gitlab-org/gitlab-ce.git' ].join('')) - expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ]) + stub_url(['http://', config.host, '/gitlab/root/gitlab-org/gitlab-ce.git'].join('')) + expect(submodule_links(submodule_item)).to eq([namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash')]) end end context 'submodule on github.com' do it 'detects ssh' do stub_url('git@github.com:gitlab-org/gitlab-ce.git') - expect(submodule_links(submodule_item)).to eq([ 'https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash' ]) + expect(submodule_links(submodule_item)).to eq(['https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash']) end it 'detects http' do stub_url('http://github.com/gitlab-org/gitlab-ce.git') - expect(submodule_links(submodule_item)).to eq([ 'https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash' ]) + expect(submodule_links(submodule_item)).to eq(['https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash']) end it 'detects https' do stub_url('https://github.com/gitlab-org/gitlab-ce.git') - expect(submodule_links(submodule_item)).to eq([ 'https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash' ]) + expect(submodule_links(submodule_item)).to eq(['https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash']) end it 'returns original with non-standard url' do stub_url('http://github.com/gitlab-org/gitlab-ce') - expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ]) + expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil]) stub_url('http://github.com/another/gitlab-org/gitlab-ce.git') - expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ]) + expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil]) end end context 'submodule on gitlab.com' do it 'detects ssh' do stub_url('git@gitlab.com:gitlab-org/gitlab-ce.git') - expect(submodule_links(submodule_item)).to eq([ 'https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash' ]) + expect(submodule_links(submodule_item)).to eq(['https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash']) end it 'detects http' do stub_url('http://gitlab.com/gitlab-org/gitlab-ce.git') - expect(submodule_links(submodule_item)).to eq([ 'https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash' ]) + expect(submodule_links(submodule_item)).to eq(['https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash']) end it 'detects https' do stub_url('https://gitlab.com/gitlab-org/gitlab-ce.git') - expect(submodule_links(submodule_item)).to eq([ 'https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash' ]) + expect(submodule_links(submodule_item)).to eq(['https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash']) end it 'returns original with non-standard url' do stub_url('http://gitlab.com/gitlab-org/gitlab-ce') - expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ]) + expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil]) stub_url('http://gitlab.com/another/gitlab-org/gitlab-ce.git') - expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ]) + expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil]) end end context 'submodule on unsupported' do it 'returns original' do stub_url('http://mygitserver.com/gitlab-org/gitlab-ce') - expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ]) + expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil]) stub_url('http://mygitserver.com/gitlab-org/gitlab-ce.git') - expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ]) + expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil]) end end diff --git a/spec/helpers/version_check_helper_spec.rb b/spec/helpers/version_check_helper_spec.rb new file mode 100644 index 00000000000..889fe441171 --- /dev/null +++ b/spec/helpers/version_check_helper_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe VersionCheckHelper do + describe '#version_status_badge' do + it 'should return nil if not dev environment and not enabled' do + allow(Rails.env).to receive(:production?) { false } + allow(current_application_settings).to receive(:version_check_enabled) { false } + + expect(helper.version_status_badge).to be(nil) + end + + context 'when production and enabled' do + before do + allow(Rails.env).to receive(:production?) { true } + allow(current_application_settings).to receive(:version_check_enabled) { true } + allow_any_instance_of(VersionCheck).to receive(:url) { 'https://version.host.com/check.svg?gitlab_info=xxx' } + + @image_tag = helper.version_status_badge + end + + it 'should return an image tag' do + expect(@image_tag).to match(/^<img/) + end + + it 'should have a js prefixed css class' do + expect(@image_tag).to match(/class="js-version-status-badge"/) + end + + it 'should have a VersionCheck url as the src' do + expect(@image_tag).to match(/src="https:\/\/version\.host\.com\/check\.svg\?gitlab_info=xxx"/) + end + end + end +end diff --git a/spec/helpers/wiki_helper_spec.rb b/spec/helpers/wiki_helper_spec.rb new file mode 100644 index 00000000000..92c6f27a867 --- /dev/null +++ b/spec/helpers/wiki_helper_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe WikiHelper do + describe '#breadcrumb' do + context 'when the page is at the root level' do + it 'returns the capitalized page name' do + slug = 'page-name' + + expect(helper.breadcrumb(slug)).to eq('Page name') + end + end + + context 'when the page is inside a directory' do + it 'returns the capitalized name of each directory and of the page itself' do + slug = 'dir_1/page-name' + + expect(helper.breadcrumb(slug)).to eq('Dir_1 / Page name') + end + end + end +end diff --git a/spec/initializers/trusted_proxies_spec.rb b/spec/initializers/trusted_proxies_spec.rb index 290e47763eb..ff8b8daa347 100644 --- a/spec/initializers/trusted_proxies_spec.rb +++ b/spec/initializers/trusted_proxies_spec.rb @@ -27,7 +27,7 @@ describe 'trusted_proxies', lib: true do context 'with private IP ranges added' do before do - set_trusted_proxies([ "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16" ]) + set_trusted_proxies(["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]) end it 'filters out private and local IPs' do @@ -39,7 +39,7 @@ describe 'trusted_proxies', lib: true do context 'with proxy IP added' do before do - set_trusted_proxies([ "60.98.25.47" ]) + set_trusted_proxies(["60.98.25.47"]) end it 'filters out proxy IP' do diff --git a/spec/javascripts/.eslintrc b/spec/javascripts/.eslintrc index fbd9bb9f0ff..3d922021978 100644 --- a/spec/javascripts/.eslintrc +++ b/spec/javascripts/.eslintrc @@ -18,7 +18,8 @@ "sandbox": false, "setFixtures": false, "setStyleFixtures": false, - "spyOnEvent": false + "spyOnEvent": false, + "ClassSpecHelper": false }, "plugins": ["jasmine"], "rules": { diff --git a/spec/javascripts/ajax_loading_spinner_spec.js b/spec/javascripts/ajax_loading_spinner_spec.js new file mode 100644 index 00000000000..a68bccb16f4 --- /dev/null +++ b/spec/javascripts/ajax_loading_spinner_spec.js @@ -0,0 +1,58 @@ +require('~/extensions/array'); +require('jquery'); +require('jquery-ujs'); +require('~/ajax_loading_spinner'); + +describe('Ajax Loading Spinner', () => { + const fixtureTemplate = 'static/ajax_loading_spinner.html.raw'; + preloadFixtures(fixtureTemplate); + + beforeEach(() => { + loadFixtures(fixtureTemplate); + gl.AjaxLoadingSpinner.init(); + }); + + it('change current icon with spinner icon and disable link while waiting ajax response', (done) => { + spyOn(jQuery, 'ajax').and.callFake((req) => { + const xhr = new XMLHttpRequest(); + const ajaxLoadingSpinner = document.querySelector('.js-ajax-loading-spinner'); + const icon = ajaxLoadingSpinner.querySelector('i'); + + req.beforeSend(xhr, { dataType: 'text/html' }); + + expect(icon).not.toHaveClass('fa-trash-o'); + expect(icon).toHaveClass('fa-spinner'); + expect(icon).toHaveClass('fa-spin'); + expect(icon.dataset.icon).toEqual('fa-trash-o'); + expect(ajaxLoadingSpinner.getAttribute('disabled')).toEqual(''); + + req.complete({}); + + done(); + const deferred = $.Deferred(); + return deferred.promise(); + }); + document.querySelector('.js-ajax-loading-spinner').click(); + }); + + it('use original icon again and enabled the link after complete the ajax request', (done) => { + spyOn(jQuery, 'ajax').and.callFake((req) => { + const xhr = new XMLHttpRequest(); + const ajaxLoadingSpinner = document.querySelector('.js-ajax-loading-spinner'); + + req.beforeSend(xhr, { dataType: 'text/html' }); + req.complete({}); + + const icon = ajaxLoadingSpinner.querySelector('i'); + expect(icon).toHaveClass('fa-trash-o'); + expect(icon).not.toHaveClass('fa-spinner'); + expect(icon).not.toHaveClass('fa-spin'); + expect(ajaxLoadingSpinner.getAttribute('disabled')).toEqual(null); + + done(); + const deferred = $.Deferred(); + return deferred.promise(); + }); + document.querySelector('.js-ajax-loading-spinner').click(); + }); +}); diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index 001cd8d6325..e5826f9c29f 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -229,4 +229,4 @@ require('./fixtures/emoji_menu'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/behaviors/autosize_spec.js b/spec/javascripts/behaviors/autosize_spec.js index 4a3da9e318b..3deaf258cae 100644 --- a/spec/javascripts/behaviors/autosize_spec.js +++ b/spec/javascripts/behaviors/autosize_spec.js @@ -18,4 +18,4 @@ require('~/behaviors/autosize'); return $(document).trigger('load'); }; }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js index b84126c0e3d..4820ce41ade 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js +++ b/spec/javascripts/behaviors/quick_submit_spec.js @@ -5,73 +5,83 @@ require('~/behaviors/quick_submit'); (function() { describe('Quick Submit behavior', function() { var keydownEvent; - preloadFixtures('static/behaviors/quick_submit.html.raw'); + preloadFixtures('issues/open-issue.html.raw'); beforeEach(function() { - loadFixtures('static/behaviors/quick_submit.html.raw'); + loadFixtures('issues/open-issue.html.raw'); $('form').submit(function(e) { // Prevent a form submit from moving us off the testing page return e.preventDefault(); }); - return this.spies = { + this.spies = { submit: spyOnEvent('form', 'submit') }; + + this.textarea = $('.js-quick-submit textarea').first(); }); it('does not respond to other keyCodes', function() { - $('input.quick-submit-input').trigger(keydownEvent({ + this.textarea.trigger(keydownEvent({ keyCode: 32 })); return expect(this.spies.submit).not.toHaveBeenTriggered(); }); it('does not respond to Enter alone', function() { - $('input.quick-submit-input').trigger(keydownEvent({ + this.textarea.trigger(keydownEvent({ ctrlKey: false, metaKey: false })); return expect(this.spies.submit).not.toHaveBeenTriggered(); }); it('does not respond to repeated events', function() { - $('input.quick-submit-input').trigger(keydownEvent({ + this.textarea.trigger(keydownEvent({ repeat: true })); return expect(this.spies.submit).not.toHaveBeenTriggered(); }); - it('disables submit buttons', function() { - $('textarea').trigger(keydownEvent()); - expect($('input[type=submit]')).toBeDisabled(); - return expect($('button[type=submit]')).toBeDisabled(); + it('disables input of type submit', function() { + const submitButton = $('.js-quick-submit input[type=submit]'); + this.textarea.trigger(keydownEvent()); + expect(submitButton).toBeDisabled(); + }); + it('disables button of type submit', function() { + // button doesn't exist in fixture, add it manually + const submitButton = $('<button type="submit">Submit it</button>'); + submitButton.insertAfter(this.textarea); + + this.textarea.trigger(keydownEvent()); + expect(submitButton).toBeDisabled(); }); - // We cannot stub `navigator.userAgent` for CI's `rake teaspoon` task, so we'll + // We cannot stub `navigator.userAgent` for CI's `rake karma` 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()); + this.textarea.trigger(keydownEvent()); return expect(this.spies.submit).toHaveBeenTriggered(); }); it('excludes other modifier keys', function() { - $('input.quick-submit-input').trigger(keydownEvent({ + this.textarea.trigger(keydownEvent({ altKey: true })); - $('input.quick-submit-input').trigger(keydownEvent({ + this.textarea.trigger(keydownEvent({ ctrlKey: true })); - $('input.quick-submit-input').trigger(keydownEvent({ + this.textarea.trigger(keydownEvent({ shiftKey: true })); return expect(this.spies.submit).not.toHaveBeenTriggered(); }); } else { it('responds to Ctrl+Enter', function() { - $('input.quick-submit-input').trigger(keydownEvent()); + this.textarea.trigger(keydownEvent()); return expect(this.spies.submit).toHaveBeenTriggered(); }); it('excludes other modifier keys', function() { - $('input.quick-submit-input').trigger(keydownEvent({ + this.textarea.trigger(keydownEvent({ altKey: true })); - $('input.quick-submit-input').trigger(keydownEvent({ + this.textarea.trigger(keydownEvent({ metaKey: true })); - $('input.quick-submit-input').trigger(keydownEvent({ + this.textarea.trigger(keydownEvent({ shiftKey: true })); return expect(this.spies.submit).not.toHaveBeenTriggered(); @@ -93,4 +103,4 @@ require('~/behaviors/quick_submit'); return $.Event('keydown', $.extend({}, defaults, options)); }; }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js index a958ac76e66..3a84013a2ed 100644 --- a/spec/javascripts/behaviors/requires_input_spec.js +++ b/spec/javascripts/behaviors/requires_input_spec.js @@ -4,18 +4,19 @@ require('~/behaviors/requires_input'); (function() { describe('requiresInput', function() { - preloadFixtures('static/behaviors/requires_input.html.raw'); + preloadFixtures('branches/new_branch.html.raw'); beforeEach(function() { - return loadFixtures('static/behaviors/requires_input.html.raw'); + loadFixtures('branches/new_branch.html.raw'); + this.submitButton = $('button[type="submit"]'); }); it('disables submit when any field is required', function() { $('.js-requires-input').requiresInput(); - return expect($('.submit')).toBeDisabled(); + return expect(this.submitButton).toBeDisabled(); }); it('enables submit when no field is required', function() { $('*[required=required]').removeAttr('required'); $('.js-requires-input').requiresInput(); - return expect($('.submit')).not.toBeDisabled(); + return expect(this.submitButton).not.toBeDisabled(); }); it('enables submit when all required fields are pre-filled', function() { $('*[required=required]').remove(); @@ -25,9 +26,9 @@ require('~/behaviors/requires_input'); it('enables submit when all required fields receive input', function() { $('.js-requires-input').requiresInput(); $('#required1').val('input1').change(); - expect($('.submit')).toBeDisabled(); + expect(this.submitButton).toBeDisabled(); $('#optional1').val('input1').change(); - expect($('.submit')).toBeDisabled(); + expect(this.submitButton).toBeDisabled(); $('#required2').val('input2').change(); $('#required3').val('input3').change(); $('#required4').val('input4').change(); @@ -35,4 +36,4 @@ require('~/behaviors/requires_input'); return expect($('.submit')).not.toBeDisabled(); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js new file mode 100644 index 00000000000..be31f644e20 --- /dev/null +++ b/spec/javascripts/boards/board_card_spec.js @@ -0,0 +1,168 @@ +/* global Vue */ +/* global List */ +/* global ListLabel */ +/* global listObj */ +/* global boardsMockInterceptor */ +/* global BoardService */ + +require('~/boards/models/list'); +require('~/boards/models/label'); +require('~/boards/stores/boards_store'); +const boardCard = require('~/boards/components/board_card').default; +require('./mock_data'); + +describe('Issue card', () => { + let vm; + + beforeEach((done) => { + Vue.http.interceptors.push(boardsMockInterceptor); + + gl.boardService = new BoardService('/test/issue-boards/board', '', '1'); + gl.issueBoards.BoardsStore.create(); + gl.issueBoards.BoardsStore.detail.issue = {}; + + const BoardCardComp = Vue.extend(boardCard); + const list = new List(listObj); + const label1 = new ListLabel({ + id: 3, + title: 'testing 123', + color: 'blue', + text_color: 'white', + description: 'test', + }); + + setTimeout(() => { + list.issues[0].labels.push(label1); + + vm = new BoardCardComp({ + propsData: { + list, + issue: list.issues[0], + issueLinkBase: '/', + disabled: false, + index: 0, + rootPath: '/', + }, + }).$mount(); + done(); + }, 0); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, boardsMockInterceptor); + }); + + it('returns false when detailIssue is empty', () => { + expect(vm.issueDetailVisible).toBe(false); + }); + + it('returns true when detailIssue is equal to card issue', () => { + gl.issueBoards.BoardsStore.detail.issue = vm.issue; + + expect(vm.issueDetailVisible).toBe(true); + }); + + it('adds user-can-drag class if not disabled', () => { + expect(vm.$el.classList.contains('user-can-drag')).toBe(true); + }); + + it('does not add user-can-drag class disabled', (done) => { + vm.disabled = true; + + setTimeout(() => { + expect(vm.$el.classList.contains('user-can-drag')).toBe(false); + done(); + }, 0); + }); + + it('does not add disabled class', () => { + expect(vm.$el.classList.contains('is-disabled')).toBe(false); + }); + + it('adds disabled class is disabled is true', (done) => { + vm.disabled = true; + + setTimeout(() => { + expect(vm.$el.classList.contains('is-disabled')).toBe(true); + done(); + }, 0); + }); + + describe('mouse events', () => { + const triggerEvent = (eventName, el = vm.$el) => { + const event = document.createEvent('MouseEvents'); + event.initMouseEvent(eventName, true, true, window, 1, 0, 0, 0, 0, false, false, + false, false, 0, null); + + el.dispatchEvent(event); + }; + + it('sets showDetail to true on mousedown', () => { + triggerEvent('mousedown'); + + expect(vm.showDetail).toBe(true); + }); + + it('sets showDetail to false on mousemove', () => { + triggerEvent('mousedown'); + + expect(vm.showDetail).toBe(true); + + triggerEvent('mousemove'); + + expect(vm.showDetail).toBe(false); + }); + + it('does not set detail issue if showDetail is false', () => { + expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({}); + }); + + it('does not set detail issue if link is clicked', () => { + triggerEvent('mouseup', vm.$el.querySelector('a')); + + expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({}); + }); + + it('does not set detail issue if button is clicked', () => { + triggerEvent('mouseup', vm.$el.querySelector('button')); + + expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({}); + }); + + it('does not set detail issue if showDetail is false after mouseup', () => { + triggerEvent('mouseup'); + + expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({}); + }); + + it('sets detail issue to card issue on mouse up', () => { + triggerEvent('mousedown'); + triggerEvent('mouseup'); + + expect(gl.issueBoards.BoardsStore.detail.issue).toEqual(vm.issue); + expect(gl.issueBoards.BoardsStore.detail.list).toEqual(vm.list); + }); + + it('adds active class if detail issue is set', (done) => { + triggerEvent('mousedown'); + triggerEvent('mouseup'); + + setTimeout(() => { + expect(vm.$el.classList.contains('is-active')).toBe(true); + done(); + }, 0); + }); + + it('resets detail issue to empty if already set', () => { + triggerEvent('mousedown'); + triggerEvent('mouseup'); + + expect(gl.issueBoards.BoardsStore.detail.issue).toEqual(vm.issue); + + triggerEvent('mousedown'); + triggerEvent('mouseup'); + + expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({}); + }); + }); +}); diff --git a/spec/javascripts/boards/board_new_issue_spec.js b/spec/javascripts/boards/board_new_issue_spec.js new file mode 100644 index 00000000000..22c9f12951b --- /dev/null +++ b/spec/javascripts/boards/board_new_issue_spec.js @@ -0,0 +1,191 @@ +/* global boardsMockInterceptor */ +/* global BoardService */ +/* global List */ +/* global listObj */ + +import Vue from 'vue'; +import boardNewIssue from '~/boards/components/board_new_issue'; + +require('~/boards/models/list'); +require('./mock_data'); +require('es6-promise').polyfill(); + +describe('Issue boards new issue form', () => { + let vm; + let list; + const promiseReturn = { + json() { + return { + iid: 100, + }; + }, + }; + const submitIssue = () => { + vm.$el.querySelector('.btn-success').click(); + }; + + beforeEach((done) => { + const BoardNewIssueComp = Vue.extend(boardNewIssue); + + Vue.http.interceptors.push(boardsMockInterceptor); + gl.boardService = new BoardService('/test/issue-boards/board', '', '1'); + gl.issueBoards.BoardsStore.create(); + gl.IssueBoardsApp = new Vue(); + + setTimeout(() => { + list = new List(listObj); + + spyOn(gl.boardService, 'newIssue').and.callFake(() => new Promise((resolve, reject) => { + if (vm.title === 'error') { + reject(); + } else { + resolve(promiseReturn); + } + })); + + vm = new BoardNewIssueComp({ + propsData: { + list, + }, + }).$mount(); + + done(); + }, 0); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, boardsMockInterceptor); + }); + + it('disables submit button if title is empty', () => { + expect(vm.$el.querySelector('.btn-success').disabled).toBe(true); + }); + + it('enables submit button if title is not empty', (done) => { + vm.title = 'Testing Title'; + + setTimeout(() => { + expect(vm.$el.querySelector('.form-control').value).toBe('Testing Title'); + expect(vm.$el.querySelector('.btn-success').disabled).not.toBe(true); + + done(); + }, 0); + }); + + it('clears title after clicking cancel', (done) => { + vm.$el.querySelector('.btn-default').click(); + + setTimeout(() => { + expect(vm.title).toBe(''); + done(); + }, 0); + }); + + it('does not create new issue if title is empty', (done) => { + submitIssue(); + + setTimeout(() => { + expect(gl.boardService.newIssue).not.toHaveBeenCalled(); + done(); + }, 0); + }); + + describe('submit success', () => { + it('creates new issue', (done) => { + vm.title = 'submit title'; + + setTimeout(() => { + submitIssue(); + + expect(gl.boardService.newIssue).toHaveBeenCalled(); + done(); + }, 0); + }); + + it('enables button after submit', (done) => { + vm.title = 'submit issue'; + + setTimeout(() => { + submitIssue(); + + expect(vm.$el.querySelector('.btn-success').disbled).not.toBe(true); + done(); + }, 0); + }); + + it('clears title after submit', (done) => { + vm.title = 'submit issue'; + + setTimeout(() => { + submitIssue(); + + expect(vm.title).toBe(''); + done(); + }, 0); + }); + + it('adds new issue to list after submit', (done) => { + vm.title = 'submit issue'; + + setTimeout(() => { + submitIssue(); + + expect(list.issues.length).toBe(2); + expect(list.issues[1].title).toBe('submit issue'); + expect(list.issues[1].subscribed).toBe(true); + done(); + }, 0); + }); + + it('sets detail issue after submit', (done) => { + vm.title = 'submit issue'; + + setTimeout(() => { + submitIssue(); + + expect(gl.issueBoards.BoardsStore.detail.issue.title).toBe('submit issue'); + done(); + }); + }); + + it('sets detail list after submit', (done) => { + vm.title = 'submit issue'; + + setTimeout(() => { + submitIssue(); + + expect(gl.issueBoards.BoardsStore.detail.list.id).toBe(list.id); + done(); + }, 0); + }); + }); + + describe('submit error', () => { + it('removes issue', (done) => { + vm.title = 'error'; + + setTimeout(() => { + submitIssue(); + + setTimeout(() => { + expect(list.issues.length).toBe(1); + done(); + }, 500); + }, 0); + }); + + it('shows error', (done) => { + vm.title = 'error'; + submitIssue(); + + setTimeout(() => { + submitIssue(); + + setTimeout(() => { + expect(vm.error).toBe(true); + done(); + }, 500); + }, 0); + }); + }); +}); diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6 index 4397a32fedc..c8a18af7198 100644 --- a/spec/javascripts/boards/list_spec.js.es6 +++ b/spec/javascripts/boards/list_spec.js.es6 @@ -3,7 +3,9 @@ /* global boardsMockInterceptor */ /* global BoardService */ /* global List */ +/* global ListIssue */ /* global listObj */ +/* global listObjDuplicate */ require('~/lib/utils/url_utility'); require('~/boards/models/issue'); @@ -84,4 +86,23 @@ describe('List model', () => { done(); }, 0); }); + + it('sends service request to update issue label', () => { + const listDup = new List(listObjDuplicate); + const issue = new ListIssue({ + title: 'Testing', + iid: 1, + confidential: false, + labels: [list.label, listDup.label] + }); + + list.issues.push(issue); + listDup.issues.push(issue); + + spyOn(gl.boardService, 'moveIssue').and.callThrough(); + + listDup.updateIssueLabel(list, issue); + + expect(gl.boardService.moveIssue).toHaveBeenCalledWith(issue.id, list.id, listDup.id); + }); }); diff --git a/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 b/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 index 789f5dc9f49..94973419979 100644 --- a/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 +++ b/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 @@ -1,10 +1,10 @@ -require('~/commit/pipelines/pipelines_store'); +const PipelinesStore = require('~/commit/pipelines/pipelines_store'); describe('Store', () => { let store; beforeEach(() => { - store = new gl.commits.pipelines.PipelinesStore(); + store = new PipelinesStore(); }); // unregister intervals and event handlers diff --git a/spec/javascripts/dashboard_spec.js.es6 b/spec/javascripts/dashboard_spec.js.es6 deleted file mode 100644 index c0bdb89ed63..00000000000 --- a/spec/javascripts/dashboard_spec.js.es6 +++ /dev/null @@ -1,37 +0,0 @@ -/* eslint-disable no-new */ - -require('~/sidebar'); -require('~/lib/utils/text_utility'); - -((global) => { - describe('Dashboard', () => { - const fixtureTemplate = 'static/dashboard.html.raw'; - - function todosCountText() { - return $('.js-todos-count').text(); - } - - function triggerToggle(newCount) { - $(document).trigger('todo:toggle', newCount); - } - - preloadFixtures(fixtureTemplate); - beforeEach(() => { - loadFixtures(fixtureTemplate); - new global.Sidebar(); - }); - - it('should update todos-count after receiving the todo:toggle event', () => { - triggerToggle(5); - expect(todosCountText()).toEqual('5'); - }); - - it('should display todos-count with delimiter', () => { - triggerToggle(1000); - expect(todosCountText()).toEqual('1,000'); - - triggerToggle(1000000); - expect(todosCountText()).toEqual('1,000,000'); - }); - }); -})(window.gl); diff --git a/spec/javascripts/environments/environment_actions_spec.js.es6 b/spec/javascripts/environments/environment_actions_spec.js.es6 index b1838045a06..d50d45d295e 100644 --- a/spec/javascripts/environments/environment_actions_spec.js.es6 +++ b/spec/javascripts/environments/environment_actions_spec.js.es6 @@ -1,4 +1,4 @@ -require('~/environments/components/environment_actions'); +const ActionsComponent = require('~/environments/components/environment_actions'); describe('Actions Component', () => { preloadFixtures('static/environments/element.html.raw'); @@ -19,11 +19,10 @@ describe('Actions Component', () => { }, ]; - const component = new window.gl.environmentsList.ActionsComponent({ + const component = new ActionsComponent({ el: document.querySelector('.test-dom-element'), propsData: { actions: actionsMock, - playIconSvg: '<svg></svg>', }, }); @@ -34,33 +33,4 @@ describe('Actions Component', () => { component.$el.querySelector('.dropdown-menu li a').getAttribute('href'), ).toEqual(actionsMock[0].play_path); }); - - it('should render a dropdown with the provided svg', () => { - const actionsMock = [ - { - name: 'bar', - play_path: 'https://gitlab.com/play', - }, - { - name: 'foo', - play_path: '#', - }, - ]; - - const component = new window.gl.environmentsList.ActionsComponent({ - el: document.querySelector('.test-dom-element'), - propsData: { - actions: actionsMock, - playIconSvg: '<svg></svg>', - }, - }); - - expect( - component.$el.querySelector('.js-dropdown-play-icon-container').children, - ).toContain('svg'); - - expect( - component.$el.querySelector('.js-action-play-icon-container').children, - ).toContain('svg'); - }); }); diff --git a/spec/javascripts/environments/environment_external_url_spec.js.es6 b/spec/javascripts/environments/environment_external_url_spec.js.es6 index a6a587e69f5..393dbb5aae0 100644 --- a/spec/javascripts/environments/environment_external_url_spec.js.es6 +++ b/spec/javascripts/environments/environment_external_url_spec.js.es6 @@ -1,4 +1,4 @@ -require('~/environments/components/environment_external_url'); +const ExternalUrlComponent = require('~/environments/components/environment_external_url'); describe('External URL Component', () => { preloadFixtures('static/environments/element.html.raw'); @@ -8,7 +8,7 @@ describe('External URL Component', () => { it('should link to the provided externalUrl prop', () => { const externalURL = 'https://gitlab.com'; - const component = new window.gl.environmentsList.ExternalUrlComponent({ + const component = new ExternalUrlComponent({ el: document.querySelector('.test-dom-element'), propsData: { externalUrl: externalURL, diff --git a/spec/javascripts/environments/environment_item_spec.js.es6 b/spec/javascripts/environments/environment_item_spec.js.es6 index d87cc0996c9..7fea80ed799 100644 --- a/spec/javascripts/environments/environment_item_spec.js.es6 +++ b/spec/javascripts/environments/environment_item_spec.js.es6 @@ -1,5 +1,5 @@ -window.timeago = require('vendor/timeago'); -require('~/environments/components/environment_item'); +window.timeago = require('timeago.js'); +const EnvironmentItem = require('~/environments/components/environment_item'); describe('Environment item', () => { preloadFixtures('static/environments/table.html.raw'); @@ -14,33 +14,16 @@ describe('Environment item', () => { beforeEach(() => { mockItem = { name: 'review', - children: [ - { - name: 'review-app', - id: 1, - state: 'available', - external_url: '', - last_deployment: {}, - created_at: '2016-11-07T11:11:16.525Z', - updated_at: '2016-11-10T15:55:58.778Z', - }, - { - name: 'production', - id: 2, - state: 'available', - external_url: '', - last_deployment: {}, - created_at: '2016-11-07T11:11:16.525Z', - updated_at: '2016-11-10T15:55:58.778Z', - }, - ], + folderName: 'review', + size: 3, + isFolder: true, + environment_path: 'url', }; - component = new window.gl.environmentsList.EnvironmentItem({ + component = new EnvironmentItem({ el: document.querySelector('tr#environment-row'), propsData: { model: mockItem, - toggleRow: () => {}, canCreateDeployment: false, canReadEnvironment: true, }, @@ -53,7 +36,7 @@ describe('Environment item', () => { }); it('Should render the number of children in a badge', () => { - expect(component.$el.querySelector('.folder-name .badge').textContent).toContain(mockItem.children.length); + expect(component.$el.querySelector('.folder-name .badge').textContent).toContain(mockItem.size); }); }); @@ -63,8 +46,8 @@ describe('Environment item', () => { beforeEach(() => { environment = { - id: 31, name: 'production', + size: 1, state: 'stopped', external_url: 'http://external.com', environment_type: null, @@ -125,11 +108,10 @@ describe('Environment item', () => { updated_at: '2016-11-10T15:55:58.778Z', }; - component = new window.gl.environmentsList.EnvironmentItem({ + component = new EnvironmentItem({ el: document.querySelector('tr#environment-row'), propsData: { model: environment, - toggleRow: () => {}, canCreateDeployment: true, canReadEnvironment: true, }, diff --git a/spec/javascripts/environments/environment_rollback_spec.js.es6 b/spec/javascripts/environments/environment_rollback_spec.js.es6 index 043b8708a6e..4a596baad09 100644 --- a/spec/javascripts/environments/environment_rollback_spec.js.es6 +++ b/spec/javascripts/environments/environment_rollback_spec.js.es6 @@ -1,4 +1,4 @@ -require('~/environments/components/environment_rollback'); +const RollbackComponent = require('~/environments/components/environment_rollback'); describe('Rollback Component', () => { preloadFixtures('static/environments/element.html.raw'); @@ -10,7 +10,7 @@ describe('Rollback Component', () => { }); it('Should link to the provided retryUrl', () => { - const component = new window.gl.environmentsList.RollbackComponent({ + const component = new RollbackComponent({ el: document.querySelector('.test-dom-element'), propsData: { retryUrl: retryURL, @@ -22,7 +22,7 @@ describe('Rollback Component', () => { }); it('Should render Re-deploy label when isLastDeployment is true', () => { - const component = new window.gl.environmentsList.RollbackComponent({ + const component = new RollbackComponent({ el: document.querySelector('.test-dom-element'), propsData: { retryUrl: retryURL, @@ -34,7 +34,7 @@ describe('Rollback Component', () => { }); it('Should render Rollback label when isLastDeployment is false', () => { - const component = new window.gl.environmentsList.RollbackComponent({ + const component = new RollbackComponent({ el: document.querySelector('.test-dom-element'), propsData: { retryUrl: retryURL, diff --git a/spec/javascripts/environments/environment_spec.js.es6 b/spec/javascripts/environments/environment_spec.js.es6 index 87eda136122..edd0cad32d0 100644 --- a/spec/javascripts/environments/environment_spec.js.es6 +++ b/spec/javascripts/environments/environment_spec.js.es6 @@ -1,9 +1,7 @@ -/* global Vue, environment */ - +const Vue = require('vue'); require('~/flash'); -require('~/environments/stores/environments_store'); -require('~/environments/components/environment'); -require('./mock_data'); +const EnvironmentsComponent = require('~/environments/components/environment'); +const { environment } = require('./mock_data'); describe('Environment', () => { preloadFixtures('static/environments/environments.html.raw'); @@ -33,11 +31,8 @@ describe('Environment', () => { }); it('should render the empty state', (done) => { - component = new gl.environmentsList.EnvironmentsComponent({ + component = new EnvironmentsComponent({ el: document.querySelector('#environments-list-view'), - propsData: { - store: gl.environmentsList.EnvironmentsStore.create(), - }, }); setTimeout(() => { @@ -54,15 +49,30 @@ describe('Environment', () => { }); }); - describe('with environments', () => { + describe('with paginated environments', () => { const environmentsResponseInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify([environment]), { + next(request.respondWith(JSON.stringify({ + environments: [environment], + stopped_count: 1, + available_count: 0, + }), { status: 200, + headers: { + 'X-nExt-pAge': '2', + 'x-page': '1', + 'X-Per-Page': '1', + 'X-Prev-Page': '', + 'X-TOTAL': '37', + 'X-Total-Pages': '2', + }, })); }; beforeEach(() => { Vue.http.interceptors.push(environmentsResponseInterceptor); + component = new EnvironmentsComponent({ + el: document.querySelector('#environments-list-view'), + }); }); afterEach(() => { @@ -72,13 +82,6 @@ describe('Environment', () => { }); it('should render a table with environments', (done) => { - component = new gl.environmentsList.EnvironmentsComponent({ - el: document.querySelector('#environments-list-view'), - propsData: { - store: gl.environmentsList.EnvironmentsStore.create(), - }, - }); - setTimeout(() => { expect( component.$el.querySelectorAll('table tbody tr').length, @@ -86,6 +89,59 @@ describe('Environment', () => { done(); }, 0); }); + + describe('pagination', () => { + it('should render pagination', (done) => { + setTimeout(() => { + expect( + component.$el.querySelectorAll('.gl-pagination li').length, + ).toEqual(5); + done(); + }, 0); + }); + + it('should update url when no search params are present', (done) => { + spyOn(gl.utils, 'visitUrl'); + setTimeout(() => { + component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); + expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2'); + done(); + }, 0); + }); + + it('should update url when page is already present', (done) => { + spyOn(gl.utils, 'visitUrl'); + window.history.pushState({}, null, '?page=1'); + + setTimeout(() => { + component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); + expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2'); + done(); + }, 0); + }); + + it('should update url when page and scope are already present', (done) => { + spyOn(gl.utils, 'visitUrl'); + window.history.pushState({}, null, '?scope=all&page=1'); + + setTimeout(() => { + component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); + expect(gl.utils.visitUrl).toHaveBeenCalledWith('?scope=all&page=2'); + done(); + }, 0); + }); + + it('should update url when page and scope are already present and page is first param', (done) => { + spyOn(gl.utils, 'visitUrl'); + window.history.pushState({}, null, '?page=1&scope=all'); + + setTimeout(() => { + component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); + expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2&scope=all'); + done(); + }, 0); + }); + }); }); }); @@ -107,11 +163,8 @@ describe('Environment', () => { }); it('should render empty state', (done) => { - component = new gl.environmentsList.EnvironmentsComponent({ + component = new EnvironmentsComponent({ el: document.querySelector('#environments-list-view'), - propsData: { - store: gl.environmentsList.EnvironmentsStore.create(), - }, }); setTimeout(() => { diff --git a/spec/javascripts/environments/environment_stop_spec.js.es6 b/spec/javascripts/environments/environment_stop_spec.js.es6 index 2dfce5ba824..5ca65b1debc 100644 --- a/spec/javascripts/environments/environment_stop_spec.js.es6 +++ b/spec/javascripts/environments/environment_stop_spec.js.es6 @@ -1,4 +1,4 @@ -require('~/environments/components/environment_stop'); +const StopComponent = require('~/environments/components/environment_stop'); describe('Stop Component', () => { preloadFixtures('static/environments/element.html.raw'); @@ -10,7 +10,7 @@ describe('Stop Component', () => { loadFixtures('static/environments/element.html.raw'); stopURL = '/stop'; - component = new window.gl.environmentsList.StopComponent({ + component = new StopComponent({ el: document.querySelector('.test-dom-element'), propsData: { stopUrl: stopURL, diff --git a/spec/javascripts/environments/environment_table_spec.js.es6 b/spec/javascripts/environments/environment_table_spec.js.es6 new file mode 100644 index 00000000000..be4330b5012 --- /dev/null +++ b/spec/javascripts/environments/environment_table_spec.js.es6 @@ -0,0 +1,30 @@ +const EnvironmentTable = require('~/environments/components/environments_table'); + +describe('Environment item', () => { + preloadFixtures('static/environments/element.html.raw'); + beforeEach(() => { + loadFixtures('static/environments/element.html.raw'); + }); + + it('Should render a table', () => { + const mockItem = { + name: 'review', + size: 3, + isFolder: true, + latest: { + environment_path: 'url', + }, + }; + + const component = new EnvironmentTable({ + el: document.querySelector('.test-dom-element'), + propsData: { + environments: [{ mockItem }], + canCreateDeployment: false, + canReadEnvironment: true, + }, + }); + + expect(component.$el.tagName).toEqual('TABLE'); + }); +}); diff --git a/spec/javascripts/environments/environments_store_spec.js.es6 b/spec/javascripts/environments/environments_store_spec.js.es6 index 9a8300d3832..77e182b3830 100644 --- a/spec/javascripts/environments/environments_store_spec.js.es6 +++ b/spec/javascripts/environments/environments_store_spec.js.es6 @@ -1,70 +1,58 @@ -/* global environmentsList */ - -require('~/environments/stores/environments_store'); -require('./mock_data'); +const Store = require('~/environments/stores/environments_store'); +const { environmentsList, serverData } = require('./mock_data'); (() => { describe('Store', () => { + let store; + beforeEach(() => { - gl.environmentsList.EnvironmentsStore.create(); + store = new Store(); }); it('should start with a blank state', () => { - expect(gl.environmentsList.EnvironmentsStore.state.environments.length).toBe(0); - expect(gl.environmentsList.EnvironmentsStore.state.stoppedCounter).toBe(0); - expect(gl.environmentsList.EnvironmentsStore.state.availableCounter).toBe(0); + expect(store.state.environments.length).toEqual(0); + expect(store.state.stoppedCounter).toEqual(0); + expect(store.state.availableCounter).toEqual(0); + expect(store.state.paginationInformation).toEqual({}); }); - describe('store environments', () => { - beforeEach(() => { - gl.environmentsList.EnvironmentsStore.storeEnvironments(environmentsList); - }); - - it('should count stopped environments and save the count in the state', () => { - expect(gl.environmentsList.EnvironmentsStore.state.stoppedCounter).toBe(1); - }); - - it('should count available environments and save the count in the state', () => { - expect(gl.environmentsList.EnvironmentsStore.state.availableCounter).toBe(3); - }); - - it('should store environments with same environment_type as sibilings', () => { - expect(gl.environmentsList.EnvironmentsStore.state.environments.length).toBe(3); - - const parentFolder = gl.environmentsList.EnvironmentsStore.state.environments - .filter(env => env.children && env.children.length > 0); - - expect(parentFolder[0].children.length).toBe(2); - expect(parentFolder[0].children[0].environment_type).toBe('review'); - expect(parentFolder[0].children[1].environment_type).toBe('review'); - expect(parentFolder[0].children[0].name).toBe('test-environment'); - expect(parentFolder[0].children[1].name).toBe('test-environment-1'); - }); - - it('should sort the environments alphabetically', () => { - const { environments } = gl.environmentsList.EnvironmentsStore.state; - - expect(environments[0].name).toBe('production'); - expect(environments[1].name).toBe('review'); - expect(environments[1].children[0].name).toBe('test-environment'); - expect(environments[1].children[1].name).toBe('test-environment-1'); - expect(environments[2].name).toBe('review_app'); - }); + it('should store environments', () => { + store.storeEnvironments(serverData); + expect(store.state.environments.length).toEqual(serverData.length); + expect(store.state.environments[0]).toEqual(environmentsList[0]); }); - describe('toggleFolder', () => { - beforeEach(() => { - gl.environmentsList.EnvironmentsStore.storeEnvironments(environmentsList); - }); - - it('should toggle the open property for the given environment', () => { - gl.environmentsList.EnvironmentsStore.toggleFolder('review'); + it('should store available count', () => { + store.storeAvailableCount(2); + expect(store.state.availableCounter).toEqual(2); + }); - const { environments } = gl.environmentsList.EnvironmentsStore.state; - const environment = environments.filter(env => env['vue-isChildren'] === true && env.name === 'review'); + it('should store stopped count', () => { + store.storeStoppedCount(2); + expect(store.state.stoppedCounter).toEqual(2); + }); - expect(environment[0].isOpen).toBe(true); - }); + it('should store pagination information', () => { + const pagination = { + 'X-nExt-pAge': '2', + 'X-page': '1', + 'X-Per-Page': '1', + 'X-Prev-Page': '2', + 'X-TOTAL': '37', + 'X-Total-Pages': '2', + }; + + const expectedResult = { + perPage: 1, + page: 1, + total: 37, + totalPages: 2, + nextPage: 2, + previousPage: 2, + }; + + store.setPagination(pagination); + expect(store.state.paginationInformation).toEqual(expectedResult); }); }); })(); diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js.es6 b/spec/javascripts/environments/folder/environments_folder_view_spec.js.es6 new file mode 100644 index 00000000000..d1335b5b304 --- /dev/null +++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js.es6 @@ -0,0 +1,202 @@ +const Vue = require('vue'); +require('~/flash'); +const EnvironmentsFolderViewComponent = require('~/environments/folder/environments_folder_view'); +const { environmentsList } = require('../mock_data'); + +describe('Environments Folder View', () => { + preloadFixtures('static/environments/environments_folder_view.html.raw'); + + beforeEach(() => { + loadFixtures('static/environments/environments_folder_view.html.raw'); + window.history.pushState({}, null, 'environments/folders/build'); + }); + + let component; + + describe('successfull request', () => { + const environmentsResponseInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify({ + environments: environmentsList, + stopped_count: 1, + available_count: 0, + }), { + status: 200, + headers: { + 'X-nExt-pAge': '2', + 'x-page': '1', + 'X-Per-Page': '1', + 'X-Prev-Page': '', + 'X-TOTAL': '37', + 'X-Total-Pages': '2', + }, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(environmentsResponseInterceptor); + component = new EnvironmentsFolderViewComponent({ + el: document.querySelector('#environments-folder-list-view'), + }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, environmentsResponseInterceptor, + ); + }); + + it('should render a table with environments', (done) => { + setTimeout(() => { + expect( + component.$el.querySelectorAll('table tbody tr').length, + ).toEqual(2); + done(); + }, 0); + }); + + it('should render available tab with count', (done) => { + setTimeout(() => { + expect( + component.$el.querySelector('.js-available-environments-folder-tab').textContent, + ).toContain('Available'); + + expect( + component.$el.querySelector('.js-available-environments-folder-tab .js-available-environments-count').textContent, + ).toContain('0'); + done(); + }, 0); + }); + + it('should render stopped tab with count', (done) => { + setTimeout(() => { + expect( + component.$el.querySelector('.js-stopped-environments-folder-tab').textContent, + ).toContain('Stopped'); + + expect( + component.$el.querySelector('.js-stopped-environments-folder-tab .js-stopped-environments-count').textContent, + ).toContain('1'); + done(); + }, 0); + }); + + it('should render parent folder name', (done) => { + setTimeout(() => { + expect( + component.$el.querySelector('.js-folder-name').textContent, + ).toContain('Environments / build'); + done(); + }, 0); + }); + + describe('pagination', () => { + it('should render pagination', (done) => { + setTimeout(() => { + expect( + component.$el.querySelectorAll('.gl-pagination li').length, + ).toEqual(5); + done(); + }, 0); + }); + + it('should update url when no search params are present', (done) => { + spyOn(gl.utils, 'visitUrl'); + setTimeout(() => { + component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); + expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2'); + done(); + }, 0); + }); + + it('should update url when page is already present', (done) => { + spyOn(gl.utils, 'visitUrl'); + window.history.pushState({}, null, '?page=1'); + + setTimeout(() => { + component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); + expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2'); + done(); + }, 0); + }); + + it('should update url when page and scope are already present', (done) => { + spyOn(gl.utils, 'visitUrl'); + window.history.pushState({}, null, '?scope=all&page=1'); + + setTimeout(() => { + component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); + expect(gl.utils.visitUrl).toHaveBeenCalledWith('?scope=all&page=2'); + done(); + }, 0); + }); + + it('should update url when page and scope are already present and page is first param', (done) => { + spyOn(gl.utils, 'visitUrl'); + window.history.pushState({}, null, '?page=1&scope=all'); + + setTimeout(() => { + component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); + expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2&scope=all'); + done(); + }, 0); + }); + }); + }); + + describe('unsuccessfull request', () => { + const environmentsErrorResponseInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify([]), { + status: 500, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(environmentsErrorResponseInterceptor); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, environmentsErrorResponseInterceptor, + ); + }); + + it('should not render a table', (done) => { + component = new EnvironmentsFolderViewComponent({ + el: document.querySelector('#environments-folder-list-view'), + }); + + setTimeout(() => { + expect( + component.$el.querySelector('table'), + ).toBe(null); + done(); + }, 0); + }); + + it('should render available tab with count 0', (done) => { + setTimeout(() => { + expect( + component.$el.querySelector('.js-available-environments-folder-tab').textContent, + ).toContain('Available'); + + expect( + component.$el.querySelector('.js-available-environments-folder-tab .js-available-environments-count').textContent, + ).toContain('0'); + done(); + }, 0); + }); + + it('should render stopped tab with count 0', (done) => { + setTimeout(() => { + expect( + component.$el.querySelector('.js-stopped-environments-folder-tab').textContent, + ).toContain('Stopped'); + + expect( + component.$el.querySelector('.js-stopped-environments-folder-tab .js-stopped-environments-count').textContent, + ).toContain('0'); + done(); + }, 0); + }); + }); +}); diff --git a/spec/javascripts/environments/mock_data.js.es6 b/spec/javascripts/environments/mock_data.js.es6 index 80e1cbc6f4d..5c395c6b2d8 100644 --- a/spec/javascripts/environments/mock_data.js.es6 +++ b/spec/javascripts/environments/mock_data.js.es6 @@ -1,153 +1,92 @@ - const environmentsList = [ { - id: 31, - name: 'production', + name: 'DEV', + size: 1, + id: 7, state: 'available', - external_url: 'https://www.gitlab.com', - environment_type: null, - last_deployment: { - id: 64, - iid: 5, - sha: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd', - ref: { - name: 'master', - ref_url: 'http://localhost:3000/root/ci-folders/tree/master', - }, - tag: false, - 'last?': true, - user: { - name: 'Administrator', - username: 'root', - id: 1, - state: 'active', - avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', - web_url: 'http://localhost:3000/root', - }, - commit: { - id: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd', - short_id: '500aabcb', - title: 'Update .gitlab-ci.yml', - author_name: 'Administrator', - author_email: 'admin@example.com', - created_at: '2016-11-07T18:28:13.000+00:00', - message: 'Update .gitlab-ci.yml', - author: { - name: 'Administrator', - username: 'root', - id: 1, - state: 'active', - avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', - web_url: 'http://localhost:3000/root', - }, - commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd', - }, - deployable: { - id: 1278, - name: 'build', - build_path: '/root/ci-folders/builds/1278', - retry_path: '/root/ci-folders/builds/1278/retry', - }, - manual_actions: [], - }, - 'stop_action?': true, - environment_path: '/root/ci-folders/environments/31', - created_at: '2016-11-07T11:11:16.525Z', - updated_at: '2016-11-07T11:11:16.525Z', - }, - { - id: 32, - name: 'review_app', - state: 'stopped', - external_url: 'https://www.gitlab.com', + external_url: null, environment_type: null, - last_deployment: { - id: 64, - iid: 5, - sha: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd', - ref: { - name: 'master', - ref_url: 'http://localhost:3000/root/ci-folders/tree/master', - }, - tag: false, - 'last?': true, - user: { - name: 'Administrator', - username: 'root', - id: 1, - state: 'active', - avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', - web_url: 'http://localhost:3000/root', - }, - commit: { - id: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd', - short_id: '500aabcb', - title: 'Update .gitlab-ci.yml', - author_name: 'Administrator', - author_email: 'admin@example.com', - created_at: '2016-11-07T18:28:13.000+00:00', - message: 'Update .gitlab-ci.yml', - author: { - name: 'Administrator', - username: 'root', - id: 1, - state: 'active', - avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', - web_url: 'http://localhost:3000/root', - }, - commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd', - }, - deployable: { - id: 1278, - name: 'build', - build_path: '/root/ci-folders/builds/1278', - retry_path: '/root/ci-folders/builds/1278/retry', - }, - manual_actions: [], - }, + last_deployment: null, 'stop_action?': false, - environment_path: '/root/ci-folders/environments/31', - created_at: '2016-11-07T11:11:16.525Z', - updated_at: '2016-11-07T11:11:16.525Z', + environment_path: '/root/review-app/environments/7', + stop_path: '/root/review-app/environments/7/stop', + created_at: '2017-01-31T10:53:46.894Z', + updated_at: '2017-01-31T10:53:46.894Z', }, { - id: 33, - name: 'test-environment', + folderName: 'build', + size: 5, + id: 12, + name: 'build/update-README', state: 'available', - environment_type: 'review', + external_url: null, + environment_type: 'build', last_deployment: null, - 'stop_action?': true, - environment_path: '/root/ci-folders/environments/31', - created_at: '2016-11-07T11:11:16.525Z', - updated_at: '2016-11-07T11:11:16.525Z', + 'stop_action?': false, + environment_path: '/root/review-app/environments/12', + stop_path: '/root/review-app/environments/12/stop', + created_at: '2017-02-01T19:42:18.400Z', + updated_at: '2017-02-01T19:42:18.400Z', }, +]; + +const serverData = [ { - id: 34, - name: 'test-environment-1', - state: 'available', - environment_type: 'review', - last_deployment: null, - 'stop_action?': true, - environment_path: '/root/ci-folders/environments/31', - created_at: '2016-11-07T11:11:16.525Z', - updated_at: '2016-11-07T11:11:16.525Z', + name: 'DEV', + size: 1, + latest: { + id: 7, + name: 'DEV', + state: 'available', + external_url: null, + environment_type: null, + last_deployment: null, + 'stop_action?': false, + environment_path: '/root/review-app/environments/7', + stop_path: '/root/review-app/environments/7/stop', + created_at: '2017-01-31T10:53:46.894Z', + updated_at: '2017-01-31T10:53:46.894Z', + }, + }, + { + name: 'build', + size: 5, + latest: { + id: 12, + name: 'build/update-README', + state: 'available', + external_url: null, + environment_type: 'build', + last_deployment: null, + 'stop_action?': false, + environment_path: '/root/review-app/environments/12', + stop_path: '/root/review-app/environments/12/stop', + created_at: '2017-02-01T19:42:18.400Z', + updated_at: '2017-02-01T19:42:18.400Z', + }, }, ]; -window.environmentsList = environmentsList; - const environment = { - id: 4, - name: 'production', - state: 'available', - external_url: 'http://production.', - environment_type: null, - last_deployment: {}, - 'stop_action?': false, - environment_path: '/root/review-app/environments/4', - stop_path: '/root/review-app/environments/4/stop', - created_at: '2016-12-16T11:51:04.690Z', - updated_at: '2016-12-16T12:04:51.133Z', + name: 'DEV', + size: 1, + latest: { + id: 7, + name: 'DEV', + state: 'available', + external_url: null, + environment_type: null, + last_deployment: null, + 'stop_action?': false, + environment_path: '/root/review-app/environments/7', + stop_path: '/root/review-app/environments/7/stop', + created_at: '2017-01-31T10:53:46.894Z', + updated_at: '2017-01-31T10:53:46.894Z', + }, }; -window.environment = environment; +module.exports = { + environmentsList, + environment, + serverData, +}; diff --git a/spec/javascripts/extensions/array_spec.js.es6 b/spec/javascripts/extensions/array_spec.js.es6 index ba5eb81defc..60f6b9b78e3 100644 --- a/spec/javascripts/extensions/array_spec.js.es6 +++ b/spec/javascripts/extensions/array_spec.js.es6 @@ -42,4 +42,4 @@ require('~/extensions/array'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/extensions/jquery_spec.js b/spec/javascripts/extensions/jquery_spec.js index c0bb0419814..096d3272eac 100644 --- a/spec/javascripts/extensions/jquery_spec.js +++ b/spec/javascripts/extensions/jquery_spec.js @@ -39,4 +39,4 @@ require('~/extensions/jquery'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 index 84c0e9cbfe2..a91801cfc89 100644 --- a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 +++ b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 @@ -99,6 +99,29 @@ require('~/filtered_search/filtered_search_tokenizer'); expect(results.tokens[2].value).toBe('Doing'); expect(results.tokens[2].symbol).toBe('~'); }); + + it('returns search value for invalid tokens', () => { + const results = gl.FilteredSearchTokenizer.processTokens('fake:token'); + expect(results.lastToken).toBe('fake:token'); + expect(results.searchToken).toBe('fake:token'); + expect(results.tokens.length).toEqual(0); + }); + + it('returns search value and token for mix of valid and invalid tokens', () => { + const results = gl.FilteredSearchTokenizer.processTokens('label:real fake:token'); + expect(results.tokens.length).toEqual(1); + expect(results.tokens[0].key).toBe('label'); + expect(results.tokens[0].value).toBe('real'); + expect(results.tokens[0].symbol).toBe(''); + expect(results.lastToken).toBe('fake:token'); + expect(results.searchToken).toBe('fake:token'); + }); + + it('returns search value for invalid symbols', () => { + const results = gl.FilteredSearchTokenizer.processTokens('std::includes'); + expect(results.lastToken).toBe('std::includes'); + expect(results.searchToken).toBe('std::includes'); + }); }); }); })(); diff --git a/spec/javascripts/fixtures/.gitignore b/spec/javascripts/fixtures/.gitignore index 009b68d5d1c..0c35cdd778e 100644 --- a/spec/javascripts/fixtures/.gitignore +++ b/spec/javascripts/fixtures/.gitignore @@ -1 +1,2 @@ *.html.raw +*.json diff --git a/spec/javascripts/fixtures/ajax_loading_spinner.html.haml b/spec/javascripts/fixtures/ajax_loading_spinner.html.haml new file mode 100644 index 00000000000..09d8c9df3b2 --- /dev/null +++ b/spec/javascripts/fixtures/ajax_loading_spinner.html.haml @@ -0,0 +1,2 @@ +%a.js-ajax-loading-spinner{href: "http://goesnowhere.nothing/whereami", data: {remote: true}} + %i.fa.fa-trash-o diff --git a/spec/javascripts/fixtures/behaviors/quick_submit.html.haml b/spec/javascripts/fixtures/behaviors/quick_submit.html.haml deleted file mode 100644 index dc2ceed42f4..00000000000 --- a/spec/javascripts/fixtures/behaviors/quick_submit.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -%form.js-quick-submit{ action: '/foo' } - %input{ type: 'text', class: 'quick-submit-input'} - %textarea - - %input{ type: 'submit'} Submit - %button.btn{ type: 'submit' } Submit diff --git a/spec/javascripts/fixtures/behaviors/requires_input.html.haml b/spec/javascripts/fixtures/behaviors/requires_input.html.haml deleted file mode 100644 index c3f905e912e..00000000000 --- a/spec/javascripts/fixtures/behaviors/requires_input.html.haml +++ /dev/null @@ -1,18 +0,0 @@ -%form.js-requires-input - %input{type: 'text', id: 'required1', required: 'required'} - %input{type: 'text', id: 'required2', required: 'required'} - %input{type: 'text', id: 'required3', required: 'required', value: 'Pre-filled'} - %input{type: 'text', id: 'optional1'} - - %textarea{id: 'required4', required: 'required'} - %textarea{id: 'optional2'} - - %select{id: 'required5', required: 'required'} - %option Zero - %option{value: '1'} One - %select{id: 'optional3', required: 'required'} - %option Zero - %option{value: '1'} One - - %button.submit{type: 'submit', value: 'Submit'} - %input.submit{type: 'submit', value: 'Submit'} diff --git a/spec/javascripts/fixtures/branches.rb b/spec/javascripts/fixtures/branches.rb new file mode 100644 index 00000000000..a059818145b --- /dev/null +++ b/spec/javascripts/fixtures/branches.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe Projects::BranchesController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') } + + render_views + + before(:all) do + clean_frontend_fixtures('branches/') + end + + before(:each) do + sign_in(admin) + end + + it 'branches/new_branch.html.raw' do |example| + get :new, + namespace_id: project.namespace.to_param, + project_id: project + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end +end diff --git a/spec/javascripts/fixtures/builds.rb b/spec/javascripts/fixtures/builds.rb index 978e25a1c32..320de791b08 100644 --- a/spec/javascripts/fixtures/builds.rb +++ b/spec/javascripts/fixtures/builds.rb @@ -24,7 +24,7 @@ describe Projects::BuildsController, '(JavaScript fixtures)', type: :controller it 'builds/build-with-artifacts.html.raw' do |example| get :show, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, id: build_with_artifacts.to_param expect(response).to be_success diff --git a/spec/javascripts/fixtures/dashboard.html.haml b/spec/javascripts/fixtures/dashboard.html.haml deleted file mode 100644 index 32446acfd60..00000000000 --- a/spec/javascripts/fixtures/dashboard.html.haml +++ /dev/null @@ -1,45 +0,0 @@ -%ul.nav.nav-sidebar - %li.home.active - %a.dashboard-shortcuts-projects - %span - Projects - %li - %a - %span - Todos - %span.count.js-todos-count - 1 - %li - %a.dashboard-shortcuts-activity - %span - Activity - %li - %a - %span - Groups - %li - %a - %span - Milestones - %li - %a.dashboard-shortcuts-issues - %span - Issues - %span - 1 - %li - %a.dashboard-shortcuts-merge_requests - %span - Merge Requests - %li - %a - %span - Snippets - %li - %a - %span - Help - %li - %a - %span - Profile Settings diff --git a/spec/javascripts/fixtures/emoji_menu.js b/spec/javascripts/fixtures/emoji_menu.js index 2ef242901e8..a50812d9517 100644 --- a/spec/javascripts/fixtures/emoji_menu.js +++ b/spec/javascripts/fixtures/emoji_menu.js @@ -1,4 +1,4 @@ /* eslint-disable space-before-function-paren */ (function() { window.emojiMenu = "<div class='emoji-menu'>\n <input type=\"text\" name=\"emoji_search\" id=\"emoji_search\" value=\"\" class=\"emoji-search search-input form-control\" />\n <div class='emoji-menu-content'>\n <h5 class='emoji-menu-title'>\n Emoticons\n </h5>\n <ul class='clearfix emoji-menu-list'>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47D\" title=\"alien\" data-aliases=\"\" data-emoji=\"alien\" data-unicode-name=\"1F47D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47C\" title=\"angel\" data-aliases=\"\" data-emoji=\"angel\" data-unicode-name=\"1F47C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A2\" title=\"anger\" data-aliases=\"\" data-emoji=\"anger\" data-unicode-name=\"1F4A2\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F620\" title=\"angry\" data-aliases=\"\" data-emoji=\"angry\" data-unicode-name=\"1F620\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F627\" title=\"anguished\" data-aliases=\"\" data-emoji=\"anguished\" data-unicode-name=\"1F627\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F632\" title=\"astonished\" data-aliases=\"\" data-emoji=\"astonished\" data-unicode-name=\"1F632\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45F\" title=\"athletic_shoe\" data-aliases=\"\" data-emoji=\"athletic_shoe\" data-unicode-name=\"1F45F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F476\" title=\"baby\" data-aliases=\"\" data-emoji=\"baby\" data-unicode-name=\"1F476\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F459\" title=\"bikini\" data-aliases=\"\" data-emoji=\"bikini\" data-unicode-name=\"1F459\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F499\" title=\"blue_heart\" data-aliases=\"\" data-emoji=\"blue_heart\" data-unicode-name=\"1F499\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60A\" title=\"blush\" data-aliases=\"\" data-emoji=\"blush\" data-unicode-name=\"1F60A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A5\" title=\"boom\" data-aliases=\"\" data-emoji=\"boom\" data-unicode-name=\"1F4A5\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F462\" title=\"boot\" data-aliases=\"\" data-emoji=\"boot\" data-unicode-name=\"1F462\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F647\" title=\"bow\" data-aliases=\"\" data-emoji=\"bow\" data-unicode-name=\"1F647\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F466\" title=\"boy\" data-aliases=\"\" data-emoji=\"boy\" data-unicode-name=\"1F466\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F470\" title=\"bride_with_veil\" data-aliases=\"\" data-emoji=\"bride_with_veil\" data-unicode-name=\"1F470\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4BC\" title=\"briefcase\" data-aliases=\"\" data-emoji=\"briefcase\" data-unicode-name=\"1F4BC\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F494\" title=\"broken_heart\" data-aliases=\"\" data-emoji=\"broken_heart\" data-unicode-name=\"1F494\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F464\" title=\"bust_in_silhouette\" data-aliases=\"\" data-emoji=\"bust_in_silhouette\" data-unicode-name=\"1F464\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F465\" title=\"busts_in_silhouette\" data-aliases=\"\" data-emoji=\"busts_in_silhouette\" data-unicode-name=\"1F465\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44F\" title=\"clap\" data-aliases=\"\" data-emoji=\"clap\" data-unicode-name=\"1F44F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F302\" title=\"closed_umbrella\" data-aliases=\"\" data-emoji=\"closed_umbrella\" data-unicode-name=\"1F302\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F630\" title=\"cold_sweat\" data-aliases=\"\" data-emoji=\"cold_sweat\" data-unicode-name=\"1F630\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F616\" title=\"confounded\" data-aliases=\"\" data-emoji=\"confounded\" data-unicode-name=\"1F616\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F615\" title=\"confused\" data-aliases=\"\" data-emoji=\"confused\" data-unicode-name=\"1F615\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F477\" title=\"construction_worker\" data-aliases=\"\" data-emoji=\"construction_worker\" data-unicode-name=\"1F477\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46E\" title=\"cop\" data-aliases=\"\" data-emoji=\"cop\" data-unicode-name=\"1F46E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46B\" title=\"couple\" data-aliases=\"\" data-emoji=\"couple\" data-unicode-name=\"1F46B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F491\" title=\"couple_with_heart\" data-aliases=\"\" data-emoji=\"couple_with_heart\" data-unicode-name=\"1F491\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48F\" title=\"couplekiss\" data-aliases=\"\" data-emoji=\"couplekiss\" data-unicode-name=\"1F48F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F451\" title=\"crown\" data-aliases=\"\" data-emoji=\"crown\" data-unicode-name=\"1F451\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F622\" title=\"cry\" data-aliases=\"\" data-emoji=\"cry\" data-unicode-name=\"1F622\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63F\" title=\"crying_cat_face\" data-aliases=\"\" data-emoji=\"crying_cat_face\" data-unicode-name=\"1F63F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F498\" title=\"cupid\" data-aliases=\"\" data-emoji=\"cupid\" data-unicode-name=\"1F498\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F483\" title=\"dancer\" data-aliases=\"\" data-emoji=\"dancer\" data-unicode-name=\"1F483\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46F\" title=\"dancers\" data-aliases=\"\" data-emoji=\"dancers\" data-unicode-name=\"1F46F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A8\" title=\"dash\" data-aliases=\"\" data-emoji=\"dash\" data-unicode-name=\"1F4A8\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61E\" title=\"disappointed\" data-aliases=\"\" data-emoji=\"disappointed\" data-unicode-name=\"1F61E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F625\" title=\"disappointed_relieved\" data-aliases=\"\" data-emoji=\"disappointed_relieved\" data-unicode-name=\"1F625\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AB\" title=\"dizzy\" data-aliases=\"\" data-emoji=\"dizzy\" data-unicode-name=\"1F4AB\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F635\" title=\"dizzy_face\" data-aliases=\"\" data-emoji=\"dizzy_face\" data-unicode-name=\"1F635\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F457\" title=\"dress\" data-aliases=\"\" data-emoji=\"dress\" data-unicode-name=\"1F457\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A7\" title=\"droplet\" data-aliases=\"\" data-emoji=\"droplet\" data-unicode-name=\"1F4A7\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F442\" title=\"ear\" data-aliases=\"\" data-emoji=\"ear\" data-unicode-name=\"1F442\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F611\" title=\"expressionless\" data-aliases=\"\" data-emoji=\"expressionless\" data-unicode-name=\"1F611\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F453\" title=\"eyeglasses\" data-aliases=\"\" data-emoji=\"eyeglasses\" data-unicode-name=\"1F453\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F440\" title=\"eyes\" data-aliases=\"\" data-emoji=\"eyes\" data-unicode-name=\"1F440\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46A\" title=\"family\" data-aliases=\"\" data-emoji=\"family\" data-unicode-name=\"1F46A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F628\" title=\"fearful\" data-aliases=\"\" data-emoji=\"fearful\" data-unicode-name=\"1F628\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F525\" title=\"fire\" data-aliases=\":flame:\" data-emoji=\"fire\" data-unicode-name=\"1F525\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270A\" title=\"fist\" data-aliases=\"\" data-emoji=\"fist\" data-unicode-name=\"270A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F633\" title=\"flushed\" data-aliases=\"\" data-emoji=\"flushed\" data-unicode-name=\"1F633\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F463\" title=\"footprints\" data-aliases=\"\" data-emoji=\"footprints\" data-unicode-name=\"1F463\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F626\" title=\"frowning\" data-aliases=\":anguished:\" data-emoji=\"frowning\" data-unicode-name=\"1F626\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48E\" title=\"gem\" data-aliases=\"\" data-emoji=\"gem\" data-unicode-name=\"1F48E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F467\" title=\"girl\" data-aliases=\"\" data-emoji=\"girl\" data-unicode-name=\"1F467\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49A\" title=\"green_heart\" data-aliases=\"\" data-emoji=\"green_heart\" data-unicode-name=\"1F49A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62C\" title=\"grimacing\" data-aliases=\"\" data-emoji=\"grimacing\" data-unicode-name=\"1F62C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F601\" title=\"grin\" data-aliases=\"\" data-emoji=\"grin\" data-unicode-name=\"1F601\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F600\" title=\"grinning\" data-aliases=\"\" data-emoji=\"grinning\" data-unicode-name=\"1F600\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F482\" title=\"guardsman\" data-aliases=\"\" data-emoji=\"guardsman\" data-unicode-name=\"1F482\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F487\" title=\"haircut\" data-aliases=\"\" data-emoji=\"haircut\" data-unicode-name=\"1F487\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45C\" title=\"handbag\" data-aliases=\"\" data-emoji=\"handbag\" data-unicode-name=\"1F45C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F649\" title=\"hear_no_evil\" data-aliases=\"\" data-emoji=\"hear_no_evil\" data-unicode-name=\"1F649\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-2764\" title=\"heart\" data-aliases=\"\" data-emoji=\"heart\" data-unicode-name=\"2764\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60D\" title=\"heart_eyes\" data-aliases=\"\" data-emoji=\"heart_eyes\" data-unicode-name=\"1F60D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63B\" title=\"heart_eyes_cat\" data-aliases=\"\" data-emoji=\"heart_eyes_cat\" data-unicode-name=\"1F63B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F493\" title=\"heartbeat\" data-aliases=\"\" data-emoji=\"heartbeat\" data-unicode-name=\"1F493\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F497\" title=\"heartpulse\" data-aliases=\"\" data-emoji=\"heartpulse\" data-unicode-name=\"1F497\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F460\" title=\"high_heel\" data-aliases=\"\" data-emoji=\"high_heel\" data-unicode-name=\"1F460\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62F\" title=\"hushed\" data-aliases=\"\" data-emoji=\"hushed\" data-unicode-name=\"1F62F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47F\" title=\"imp\" data-aliases=\"\" data-emoji=\"imp\" data-unicode-name=\"1F47F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F481\" title=\"information_desk_person\" data-aliases=\"\" data-emoji=\"information_desk_person\" data-unicode-name=\"1F481\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F607\" title=\"innocent\" data-aliases=\"\" data-emoji=\"innocent\" data-unicode-name=\"1F607\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47A\" title=\"japanese_goblin\" data-aliases=\"\" data-emoji=\"japanese_goblin\" data-unicode-name=\"1F47A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F479\" title=\"japanese_ogre\" data-aliases=\"\" data-emoji=\"japanese_ogre\" data-unicode-name=\"1F479\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F456\" title=\"jeans\" data-aliases=\"\" data-emoji=\"jeans\" data-unicode-name=\"1F456\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F602\" title=\"joy\" data-aliases=\"\" data-emoji=\"joy\" data-unicode-name=\"1F602\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F639\" title=\"joy_cat\" data-aliases=\"\" data-emoji=\"joy_cat\" data-unicode-name=\"1F639\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F458\" title=\"kimono\" data-aliases=\"\" data-emoji=\"kimono\" data-unicode-name=\"1F458\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48B\" title=\"kiss\" data-aliases=\"\" data-emoji=\"kiss\" data-unicode-name=\"1F48B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F617\" title=\"kissing\" data-aliases=\"\" data-emoji=\"kissing\" data-unicode-name=\"1F617\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63D\" title=\"kissing_cat\" data-aliases=\"\" data-emoji=\"kissing_cat\" data-unicode-name=\"1F63D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61A\" title=\"kissing_closed_eyes\" data-aliases=\"\" data-emoji=\"kissing_closed_eyes\" data-unicode-name=\"1F61A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F618\" title=\"kissing_heart\" data-aliases=\"\" data-emoji=\"kissing_heart\" data-unicode-name=\"1F618\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F619\" title=\"kissing_smiling_eyes\" data-aliases=\"\" data-emoji=\"kissing_smiling_eyes\" data-unicode-name=\"1F619\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F606\" title=\"laughing\" data-aliases=\":satisfied:\" data-emoji=\"laughing\" data-unicode-name=\"1F606\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F444\" title=\"lips\" data-aliases=\"\" data-emoji=\"lips\" data-unicode-name=\"1F444\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F484\" title=\"lipstick\" data-aliases=\"\" data-emoji=\"lipstick\" data-unicode-name=\"1F484\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48C\" title=\"love_letter\" data-aliases=\"\" data-emoji=\"love_letter\" data-unicode-name=\"1F48C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F468\" title=\"man\" data-aliases=\"\" data-emoji=\"man\" data-unicode-name=\"1F468\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F472\" title=\"man_with_gua_pi_mao\" data-aliases=\"\" data-emoji=\"man_with_gua_pi_mao\" data-unicode-name=\"1F472\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F473\" title=\"man_with_turban\" data-aliases=\"\" data-emoji=\"man_with_turban\" data-unicode-name=\"1F473\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45E\" title=\"mans_shoe\" data-aliases=\"\" data-emoji=\"mans_shoe\" data-unicode-name=\"1F45E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F637\" title=\"mask\" data-aliases=\"\" data-emoji=\"mask\" data-unicode-name=\"1F637\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F486\" title=\"massage\" data-aliases=\"\" data-emoji=\"massage\" data-unicode-name=\"1F486\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AA\" title=\"muscle\" data-aliases=\"\" data-emoji=\"muscle\" data-unicode-name=\"1F4AA\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F485\" title=\"nail_care\" data-aliases=\"\" data-emoji=\"nail_care\" data-unicode-name=\"1F485\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F454\" title=\"necktie\" data-aliases=\"\" data-emoji=\"necktie\" data-unicode-name=\"1F454\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F610\" title=\"neutral_face\" data-aliases=\"\" data-emoji=\"neutral_face\" data-unicode-name=\"1F610\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F645\" title=\"no_good\" data-aliases=\"\" data-emoji=\"no_good\" data-unicode-name=\"1F645\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F636\" title=\"no_mouth\" data-aliases=\"\" data-emoji=\"no_mouth\" data-unicode-name=\"1F636\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F443\" title=\"nose\" data-aliases=\"\" data-emoji=\"nose\" data-unicode-name=\"1F443\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44C\" title=\"ok_hand\" data-aliases=\"\" data-emoji=\"ok_hand\" data-unicode-name=\"1F44C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F646\" title=\"ok_woman\" data-aliases=\"\" data-emoji=\"ok_woman\" data-unicode-name=\"1F646\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F474\" title=\"older_man\" data-aliases=\"\" data-emoji=\"older_man\" data-unicode-name=\"1F474\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F475\" title=\"older_woman\" data-aliases=\":grandma:\" data-emoji=\"older_woman\" data-unicode-name=\"1F475\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F450\" title=\"open_hands\" data-aliases=\"\" data-emoji=\"open_hands\" data-unicode-name=\"1F450\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62E\" title=\"open_mouth\" data-aliases=\"\" data-emoji=\"open_mouth\" data-unicode-name=\"1F62E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F614\" title=\"pensive\" data-aliases=\"\" data-emoji=\"pensive\" data-unicode-name=\"1F614\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F623\" title=\"persevere\" data-aliases=\"\" data-emoji=\"persevere\" data-unicode-name=\"1F623\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64D\" title=\"person_frowning\" data-aliases=\"\" data-emoji=\"person_frowning\" data-unicode-name=\"1F64D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F471\" title=\"person_with_blond_hair\" data-aliases=\"\" data-emoji=\"person_with_blond_hair\" data-unicode-name=\"1F471\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64E\" title=\"person_with_pouting_face\" data-aliases=\"\" data-emoji=\"person_with_pouting_face\" data-unicode-name=\"1F64E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F447\" title=\"point_down\" data-aliases=\"\" data-emoji=\"point_down\" data-unicode-name=\"1F447\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F448\" title=\"point_left\" data-aliases=\"\" data-emoji=\"point_left\" data-unicode-name=\"1F448\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F449\" title=\"point_right\" data-aliases=\"\" data-emoji=\"point_right\" data-unicode-name=\"1F449\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-261D\" title=\"point_up\" data-aliases=\"\" data-emoji=\"point_up\" data-unicode-name=\"261D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F446\" title=\"point_up_2\" data-aliases=\"\" data-emoji=\"point_up_2\" data-unicode-name=\"1F446\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A9\" title=\"poop\" data-aliases=\":shit: :hankey: :poo:\" data-emoji=\"poop\" data-unicode-name=\"1F4A9\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45D\" title=\"pouch\" data-aliases=\"\" data-emoji=\"pouch\" data-unicode-name=\"1F45D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63E\" title=\"pouting_cat\" data-aliases=\"\" data-emoji=\"pouting_cat\" data-unicode-name=\"1F63E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64F\" title=\"pray\" data-aliases=\"\" data-emoji=\"pray\" data-unicode-name=\"1F64F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F478\" title=\"princess\" data-aliases=\"\" data-emoji=\"princess\" data-unicode-name=\"1F478\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44A\" title=\"punch\" data-aliases=\"\" data-emoji=\"punch\" data-unicode-name=\"1F44A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49C\" title=\"purple_heart\" data-aliases=\"\" data-emoji=\"purple_heart\" data-unicode-name=\"1F49C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45B\" title=\"purse\" data-aliases=\"\" data-emoji=\"purse\" data-unicode-name=\"1F45B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F621\" title=\"rage\" data-aliases=\"\" data-emoji=\"rage\" data-unicode-name=\"1F621\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270B\" title=\"raised_hand\" data-aliases=\"\" data-emoji=\"raised_hand\" data-unicode-name=\"270B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64C\" title=\"raised_hands\" data-aliases=\"\" data-emoji=\"raised_hands\" data-unicode-name=\"1F64C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64B\" title=\"raising_hand\" data-aliases=\"\" data-emoji=\"raising_hand\" data-unicode-name=\"1F64B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-263A\" title=\"relaxed\" data-aliases=\"\" data-emoji=\"relaxed\" data-unicode-name=\"263A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60C\" title=\"relieved\" data-aliases=\"\" data-emoji=\"relieved\" data-unicode-name=\"1F60C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49E\" title=\"revolving_hearts\" data-aliases=\"\" data-emoji=\"revolving_hearts\" data-unicode-name=\"1F49E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F380\" title=\"ribbon\" data-aliases=\"\" data-emoji=\"ribbon\" data-unicode-name=\"1F380\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48D\" title=\"ring\" data-aliases=\"\" data-emoji=\"ring\" data-unicode-name=\"1F48D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3C3\" title=\"runner\" data-aliases=\"\" data-emoji=\"runner\" data-unicode-name=\"1F3C3\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3BD\" title=\"running_shirt_with_sash\" data-aliases=\"\" data-emoji=\"running_shirt_with_sash\" data-unicode-name=\"1F3BD\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F461\" title=\"sandal\" data-aliases=\"\" data-emoji=\"sandal\" data-unicode-name=\"1F461\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F631\" title=\"scream\" data-aliases=\"\" data-emoji=\"scream\" data-unicode-name=\"1F631\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F640\" title=\"scream_cat\" data-aliases=\"\" data-emoji=\"scream_cat\" data-unicode-name=\"1F640\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F648\" title=\"see_no_evil\" data-aliases=\"\" data-emoji=\"see_no_evil\" data-unicode-name=\"1F648\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F455\" title=\"shirt\" data-aliases=\"\" data-emoji=\"shirt\" data-unicode-name=\"1F455\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F480\" title=\"skull\" data-aliases=\":skeleton:\" data-emoji=\"skull\" data-unicode-name=\"1F480\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F634\" title=\"sleeping\" data-aliases=\"\" data-emoji=\"sleeping\" data-unicode-name=\"1F634\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62A\" title=\"sleepy\" data-aliases=\"\" data-emoji=\"sleepy\" data-unicode-name=\"1F62A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F604\" title=\"smile\" data-aliases=\"\" data-emoji=\"smile\" data-unicode-name=\"1F604\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F638\" title=\"smile_cat\" data-aliases=\"\" data-emoji=\"smile_cat\" data-unicode-name=\"1F638\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F603\" title=\"smiley\" data-aliases=\"\" data-emoji=\"smiley\" data-unicode-name=\"1F603\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63A\" title=\"smiley_cat\" data-aliases=\"\" data-emoji=\"smiley_cat\" data-unicode-name=\"1F63A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F608\" title=\"smiling_imp\" data-aliases=\"\" data-emoji=\"smiling_imp\" data-unicode-name=\"1F608\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60F\" title=\"smirk\" data-aliases=\"\" data-emoji=\"smirk\" data-unicode-name=\"1F60F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63C\" title=\"smirk_cat\" data-aliases=\"\" data-emoji=\"smirk_cat\" data-unicode-name=\"1F63C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62D\" title=\"sob\" data-aliases=\"\" data-emoji=\"sob\" data-unicode-name=\"1F62D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-2728\" title=\"sparkles\" data-aliases=\"\" data-emoji=\"sparkles\" data-unicode-name=\"2728\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F496\" title=\"sparkling_heart\" data-aliases=\"\" data-emoji=\"sparkling_heart\" data-unicode-name=\"1F496\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64A\" title=\"speak_no_evil\" data-aliases=\"\" data-emoji=\"speak_no_evil\" data-unicode-name=\"1F64A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AC\" title=\"speech_balloon\" data-aliases=\"\" data-emoji=\"speech_balloon\" data-unicode-name=\"1F4AC\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F31F\" title=\"star2\" data-aliases=\"\" data-emoji=\"star2\" data-unicode-name=\"1F31F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61B\" title=\"stuck_out_tongue\" data-aliases=\"\" data-emoji=\"stuck_out_tongue\" data-unicode-name=\"1F61B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61D\" title=\"stuck_out_tongue_closed_eyes\" data-aliases=\"\" data-emoji=\"stuck_out_tongue_closed_eyes\" data-unicode-name=\"1F61D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61C\" title=\"stuck_out_tongue_winking_eye\" data-aliases=\"\" data-emoji=\"stuck_out_tongue_winking_eye\" data-unicode-name=\"1F61C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60E\" title=\"sunglasses\" data-aliases=\"\" data-emoji=\"sunglasses\" data-unicode-name=\"1F60E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F613\" title=\"sweat\" data-aliases=\"\" data-emoji=\"sweat\" data-unicode-name=\"1F613\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A6\" title=\"sweat_drops\" data-aliases=\"\" data-emoji=\"sweat_drops\" data-unicode-name=\"1F4A6\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F605\" title=\"sweat_smile\" data-aliases=\"\" data-emoji=\"sweat_smile\" data-unicode-name=\"1F605\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AD\" title=\"thought_balloon\" data-aliases=\"\" data-emoji=\"thought_balloon\" data-unicode-name=\"1F4AD\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44E\" title=\"thumbsdown\" data-aliases=\":-1:\" data-emoji=\"thumbsdown\" data-unicode-name=\"1F44E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44D\" title=\"thumbsup\" data-aliases=\":+1:\" data-emoji=\"thumbsup\" data-unicode-name=\"1F44D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62B\" title=\"tired_face\" data-aliases=\"\" data-emoji=\"tired_face\" data-unicode-name=\"1F62B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F445\" title=\"tongue\" data-aliases=\"\" data-emoji=\"tongue\" data-unicode-name=\"1F445\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3A9\" title=\"tophat\" data-aliases=\"\" data-emoji=\"tophat\" data-unicode-name=\"1F3A9\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F624\" title=\"triumph\" data-aliases=\"\" data-emoji=\"triumph\" data-unicode-name=\"1F624\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F495\" title=\"two_hearts\" data-aliases=\"\" data-emoji=\"two_hearts\" data-unicode-name=\"1F495\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46C\" title=\"two_men_holding_hands\" data-aliases=\"\" data-emoji=\"two_men_holding_hands\" data-unicode-name=\"1F46C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46D\" title=\"two_women_holding_hands\" data-aliases=\"\" data-emoji=\"two_women_holding_hands\" data-unicode-name=\"1F46D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F612\" title=\"unamused\" data-aliases=\"\" data-emoji=\"unamused\" data-unicode-name=\"1F612\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270C\" title=\"v\" data-aliases=\"\" data-emoji=\"v\" data-unicode-name=\"270C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F6B6\" title=\"walking\" data-aliases=\"\" data-emoji=\"walking\" data-unicode-name=\"1F6B6\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44B\" title=\"wave\" data-aliases=\"\" data-emoji=\"wave\" data-unicode-name=\"1F44B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F629\" title=\"weary\" data-aliases=\"\" data-emoji=\"weary\" data-unicode-name=\"1F629\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F609\" title=\"wink\" data-aliases=\"\" data-emoji=\"wink\" data-unicode-name=\"1F609\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F469\" title=\"woman\" data-aliases=\"\" data-emoji=\"woman\" data-unicode-name=\"1F469\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45A\" title=\"womans_clothes\" data-aliases=\"\" data-emoji=\"womans_clothes\" data-unicode-name=\"1F45A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F452\" title=\"womans_hat\" data-aliases=\"\" data-emoji=\"womans_hat\" data-unicode-name=\"1F452\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61F\" title=\"worried\" data-aliases=\"\" data-emoji=\"worried\" data-unicode-name=\"1F61F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49B\" title=\"yellow_heart\" data-aliases=\"\" data-emoji=\"yellow_heart\" data-unicode-name=\"1F49B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60B\" title=\"yum\" data-aliases=\"\" data-emoji=\"yum\" data-unicode-name=\"1F60B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A4\" title=\"zzz\" data-aliases=\"\" data-emoji=\"zzz\" data-unicode-name=\"1F4A4\"></div>\n </button>\n </li>\n </ul>\n </div>\n</div>"; -}).call(this); +}).call(window); diff --git a/spec/javascripts/fixtures/environments/environments_folder_view.html.haml b/spec/javascripts/fixtures/environments/environments_folder_view.html.haml new file mode 100644 index 00000000000..aceec139730 --- /dev/null +++ b/spec/javascripts/fixtures/environments/environments_folder_view.html.haml @@ -0,0 +1,7 @@ +%div + #environments-folder-list-view{ data: { "can-create-deployment" => "true", + "can-read-environment" => "true", + "css-class" => "", + "commit-icon-svg" => custom_icon("icon_commit"), + "terminal-icon-svg" => custom_icon("icon_terminal"), + "play-icon-svg" => custom_icon("icon_play") } } diff --git a/spec/javascripts/fixtures/header.html.haml b/spec/javascripts/fixtures/header.html.haml deleted file mode 100644 index 4db2ef604de..00000000000 --- a/spec/javascripts/fixtures/header.html.haml +++ /dev/null @@ -1,35 +0,0 @@ -%header.navbar.navbar-fixed-top.navbar-gitlab.nav_header_class - .container-fluid - .header-content - %button.side-nav-toggle - %span.sr-only - Toggle navigation - %i.fa.fa-bars - %button.navbar-toggle - %span.sr-only - Toggle navigation - %i.fa.fa-ellipsis-v - .navbar-collapse.collapse - %ui.nav.navbar-nav - %li.hidden-sm.hidden-xs - %li.visible-sm.visible-xs - %li - %a - %i.fa.fa-bell.fa-fw - %span.badge.todos-pending-count - %li - %a - %i.fa.fa-plus.fa-fw - %li.header-user.dropdown - %a - %img - %span.caret - .dropdown-menu-nav - .dropdown-menu-align-right - %ul - %li - %a.profile-link - %li - %a - %li.divider - %li.sign-out-link diff --git a/spec/javascripts/fixtures/issues.rb b/spec/javascripts/fixtures/issues.rb index 06f708f9e15..88e3f860809 100644 --- a/spec/javascripts/fixtures/issues.rb +++ b/spec/javascripts/fixtures/issues.rb @@ -41,7 +41,7 @@ describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller def render_issue(fixture_file_name, issue) get :show, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project, id: issue.to_param expect(response).to be_success diff --git a/spec/javascripts/fixtures/merge_request_tabs.html.haml b/spec/javascripts/fixtures/merge_request_tabs.html.haml deleted file mode 100644 index 68678c3d7e3..00000000000 --- a/spec/javascripts/fixtures/merge_request_tabs.html.haml +++ /dev/null @@ -1,22 +0,0 @@ -%ul.nav.nav-tabs.merge-request-tabs - %li.notes-tab - %a{href: '/foo/bar/merge_requests/1', data: {target: 'div#notes', action: 'notes', toggle: 'tab'}} - Discussion - %li.commits-tab - %a{href: '/foo/bar/merge_requests/1/commits', data: {target: 'div#commits', action: 'commits', toggle: 'tab'}} - Commits - %li.diffs-tab - %a{href: '/foo/bar/merge_requests/1/diffs', data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'}} - Diffs - -.tab-content - #notes.notes.tab-pane - Notes Content - #commits.commits.tab-pane - Commits Content - #diffs.diffs.tab-pane - Diffs Content - -.mr-loading-status - .loading - Loading Animation diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb new file mode 100644 index 00000000000..ee893b76c84 --- /dev/null +++ b/spec/javascripts/fixtures/merge_requests.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project, namespace: namespace, path: 'merge-requests-project') } + + render_views + + before(:all) do + clean_frontend_fixtures('merge_requests/') + end + + before(:each) do + sign_in(admin) + end + + it 'merge_requests/merge_request_with_task_list.html.raw' do |example| + merge_request = create(:merge_request, :with_diffs, source_project: project, target_project: project, description: '- [ ] Task List Item') + render_merge_request(example.description, merge_request) + end + + private + + def render_merge_request(fixture_file_name, merge_request) + get :show, + namespace_id: project.namespace.to_param, + project_id: project, + id: merge_request.to_param + + expect(response).to be_success + store_frontend_fixture(response, fixture_file_name) + end +end diff --git a/spec/javascripts/fixtures/new_branch.html.haml b/spec/javascripts/fixtures/new_branch.html.haml deleted file mode 100644 index f06629e5ecc..00000000000 --- a/spec/javascripts/fixtures/new_branch.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -%form.js-create-branch-form - %input.js-branch-name - .js-branch-name-error - %input{id: "ref"} diff --git a/spec/javascripts/fixtures/project_title.html.haml b/spec/javascripts/fixtures/project_title.html.haml deleted file mode 100644 index 9d1f7877116..00000000000 --- a/spec/javascripts/fixtures/project_title.html.haml +++ /dev/null @@ -1,20 +0,0 @@ -.header-content - %h1.title - %a - GitLab Org - %a.project-item-select-holder{href: "/gitlab-org/gitlab-test"} - GitLab Test - %i.fa.chevron-down.dropdown-toggle-caret.js-projects-dropdown-toggle{ "data-toggle" => "dropdown", "data-target" => ".header-content", "data-order-by" => "last_activity_at" } - .js-dropdown-menu-projects - .dropdown-menu.dropdown-select.dropdown-menu-projects - .dropdown-title - %span Go to a project - %button.dropdown-title-button.dropdown-menu-close{"aria-label" => "Close", type: "button"} - %i.fa.fa-times.dropdown-menu-close-icon - .dropdown-input - %input.dropdown-input-field{id: "", placeholder: "Search your projects", type: "search", value: ""} - %i.fa.fa-search.dropdown-input-search - %i.fa.fa-times.dropdown-input-clear.js-dropdown-input-clear{role: "button"} - .dropdown-content - .dropdown-loading - %i.fa.fa-spinner.fa-spin diff --git a/spec/javascripts/fixtures/projects.json b/spec/javascripts/fixtures/projects.json index 4ce7f5c601a..1339ee00870 100644 --- a/spec/javascripts/fixtures/projects.json +++ b/spec/javascripts/fixtures/projects.json @@ -43,7 +43,7 @@ "avatar_url": null, "star_count": 0, "forks_count": 0, - "only_allow_merge_if_build_succeeds": false, + "only_allow_merge_if_pipeline_succeeds": false, "open_issues_count": 0, "permissions": { "project_access": null, @@ -88,7 +88,7 @@ "avatar_url": null, "star_count": 0, "forks_count": 0, - "only_allow_merge_if_build_succeeds": false, + "only_allow_merge_if_pipeline_succeeds": false, "open_issues_count": 5, "permissions": { "project_access": { @@ -139,7 +139,7 @@ "avatar_url": null, "star_count": 0, "forks_count": 0, - "only_allow_merge_if_build_succeeds": true, + "only_allow_merge_if_pipeline_succeeds": true, "open_issues_count": 4, "permissions": { "project_access": null, @@ -187,7 +187,7 @@ "avatar_url": null, "star_count": 0, "forks_count": 0, - "only_allow_merge_if_build_succeeds": true, + "only_allow_merge_if_pipeline_succeeds": true, "open_issues_count": 4, "permissions": { "project_access": null, @@ -235,7 +235,7 @@ "avatar_url": null, "star_count": 0, "forks_count": 0, - "only_allow_merge_if_build_succeeds": false, + "only_allow_merge_if_pipeline_succeeds": false, "open_issues_count": 5, "permissions": { "project_access": null, @@ -283,7 +283,7 @@ "avatar_url": null, "star_count": 0, "forks_count": 0, - "only_allow_merge_if_build_succeeds": false, + "only_allow_merge_if_pipeline_succeeds": false, "open_issues_count": 5, "permissions": { "project_access": { @@ -334,7 +334,7 @@ "avatar_url": null, "star_count": 0, "forks_count": 0, - "only_allow_merge_if_build_succeeds": false, + "only_allow_merge_if_pipeline_succeeds": false, "open_issues_count": 3, "permissions": { "project_access": null, @@ -382,7 +382,7 @@ "avatar_url": null, "star_count": 0, "forks_count": 0, - "only_allow_merge_if_build_succeeds": false, + "only_allow_merge_if_pipeline_succeeds": false, "open_issues_count": 5, "permissions": { "project_access": { @@ -433,7 +433,7 @@ "avatar_url": null, "star_count": 0, "forks_count": 0, - "only_allow_merge_if_build_succeeds": false, + "only_allow_merge_if_pipeline_succeeds": false, "open_issues_count": 5, "permissions": { "project_access": null, diff --git a/spec/javascripts/fixtures/projects.rb b/spec/javascripts/fixtures/projects.rb index 56513219e1e..6c33b240e5c 100644 --- a/spec/javascripts/fixtures/projects.rb +++ b/spec/javascripts/fixtures/projects.rb @@ -20,7 +20,7 @@ describe ProjectsController, '(JavaScript fixtures)', type: :controller do it 'projects/dashboard.html.raw' do |example| get :show, namespace_id: project.namespace.to_param, - id: project.to_param + id: project expect(response).to be_success store_frontend_fixture(response, example.description) diff --git a/spec/javascripts/fixtures/todos.json b/spec/javascripts/fixtures/todos.json deleted file mode 100644 index 62c2387d515..00000000000 --- a/spec/javascripts/fixtures/todos.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "count": 1, - "delete_path": "/dashboard/todos/1" -}
\ No newline at end of file diff --git a/spec/javascripts/fixtures/todos.rb b/spec/javascripts/fixtures/todos.rb new file mode 100644 index 00000000000..a81ef8c5492 --- /dev/null +++ b/spec/javascripts/fixtures/todos.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +describe 'Todos (JavaScript fixtures)' do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project_empty_repo, namespace: namespace, path: 'todos-project') } + let(:issue_1) { create(:issue, title: 'issue_1', project: project) } + let!(:todo_1) { create(:todo, user: admin, project: project, target: issue_1, created_at: 5.hours.ago) } + let(:issue_2) { create(:issue, title: 'issue_2', project: project) } + let!(:todo_2) { create(:todo, :done, user: admin, project: project, target: issue_2, created_at: 50.hours.ago) } + + before(:all) do + clean_frontend_fixtures('todos/') + end + + describe Dashboard::TodosController, '(JavaScript fixtures)', type: :controller do + render_views + + before(:each) do + sign_in(admin) + end + + it 'todos/todos.html.raw' do |example| + get :index + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end + end + + describe Projects::TodosController, '(JavaScript fixtures)', type: :controller do + render_views + + before(:each) do + sign_in(admin) + end + + it 'todos/todos.json' do |example| + post :create, + namespace_id: namespace, + project_id: project, + issuable_type: 'issue', + issuable_id: issue_2.id, + format: 'json' + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end + end +end diff --git a/spec/javascripts/fixtures/user_callout.html.haml b/spec/javascripts/fixtures/user_callout.html.haml new file mode 100644 index 00000000000..275359bde0a --- /dev/null +++ b/spec/javascripts/fixtures/user_callout.html.haml @@ -0,0 +1,2 @@ +.user-callout{ 'callout-svg' => custom_icon('icon_customization') } + diff --git a/spec/javascripts/gfm_auto_complete_spec.js.es6 b/spec/javascripts/gfm_auto_complete_spec.js.es6 index c61c32f8a13..5dfa4008fbd 100644 --- a/spec/javascripts/gfm_auto_complete_spec.js.es6 +++ b/spec/javascripts/gfm_auto_complete_spec.js.es6 @@ -1,3 +1,5 @@ +/* eslint no-param-reassign: "off" */ + require('~/gfm_auto_complete'); require('vendor/jquery.caret'); require('vendor/jquery.atwho'); @@ -63,6 +65,61 @@ describe('GfmAutoComplete', function () { }); }); + describe('DefaultOptions.matcher', function () { + const defaultMatcher = (context, flag, subtext) => ( + GfmAutoComplete.DefaultOptions.matcher.call(context, flag, subtext) + ); + + const flagsUseDefaultMatcher = ['@', '#', '!', '~', '%']; + const otherFlags = ['/', ':']; + const flags = flagsUseDefaultMatcher.concat(otherFlags); + + const flagsHash = flags.reduce((hash, el) => { hash[el] = null; return hash; }, {}); + const atwhoInstance = { setting: {}, app: { controllers: flagsHash } }; + + const minLen = 1; + const maxLen = 20; + const argumentSize = [minLen, maxLen / 2, maxLen]; + + const allowedSymbols = ['', 'a', 'n', 'z', 'A', 'Z', 'N', '0', '5', '9', 'А', 'а', 'Я', 'я', '.', '\'', '+', '-', '_']; + const jointAllowedSymbols = allowedSymbols.join(''); + + describe('should match regular symbols', () => { + flagsUseDefaultMatcher.forEach((flag) => { + allowedSymbols.forEach((symbol) => { + argumentSize.forEach((size) => { + const query = new Array(size + 1).join(symbol); + const subtext = flag + query; + + it(`matches argument "${flag}" with query "${subtext}"`, () => { + expect(defaultMatcher(atwhoInstance, flag, subtext)).toBe(query); + }); + }); + }); + + it(`matches combination of allowed symbols for flag "${flag}"`, () => { + const subtext = flag + jointAllowedSymbols; + + expect(defaultMatcher(atwhoInstance, flag, subtext)).toBe(jointAllowedSymbols); + }); + }); + }); + + describe('should not match special sequences', () => { + const ShouldNotBeFollowedBy = flags.concat(['\x00', '\x10', '\x3f', '\n', ' ']); + + flagsUseDefaultMatcher.forEach((atSign) => { + ShouldNotBeFollowedBy.forEach((followedSymbol) => { + const seq = atSign + followedSymbol; + + it(`should not match "${seq}"`, () => { + expect(defaultMatcher(atwhoInstance, atSign, seq)).toBe(null); + }); + }); + }); + }); + }); + describe('isLoading', function () { it('should be true with loading data object item', function () { expect(GfmAutoComplete.isLoading({ name: 'loading' })).toBe(true); diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6 index 317f38c5888..c207fb00a47 100644 --- a/spec/javascripts/gl_dropdown_spec.js.es6 +++ b/spec/javascripts/gl_dropdown_spec.js.es6 @@ -139,6 +139,14 @@ require('~/lib/utils/url_utility'); this.dropdownButtonElement.click(); }); + it('should show loading indicator while search results are being fetched by backend', () => { + const dropdownMenu = document.querySelector('.dropdown-menu'); + + expect(dropdownMenu.className.indexOf('is-loading') !== -1).toEqual(true); + remoteCallback(); + expect(dropdownMenu.className.indexOf('is-loading') !== -1).toEqual(false); + }); + it('should not focus search input while remote task is not complete', () => { expect($(document.activeElement)).not.toEqual($(SEARCH_INPUT_SELECTOR)); remoteCallback(); diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js index a954bb60560..861f26e162f 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js @@ -1,9 +1,7 @@ -/* eslint-disable quotes, jasmine/no-suite-dupes, vars-on-top, no-var, max-len */ -/* global d3 */ -/* global ContributorsGraph */ -/* global ContributorsMasterGraph */ +/* eslint-disable quotes, jasmine/no-suite-dupes, vars-on-top, no-var */ -require('~/graphs/stat_graph_contributors_graph'); +import d3 from 'd3'; +import { ContributorsGraph, ContributorsMasterGraph } from '~/graphs/stat_graph_contributors_graph'; describe("ContributorsGraph", function () { describe("#set_x_domain", function () { diff --git a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js index b15764abe8c..9b47ab62181 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js @@ -1,7 +1,6 @@ /* eslint-disable quotes, no-var, camelcase, object-property-newline, comma-dangle, max-len, vars-on-top, quote-props */ -/* global ContributorsStatGraphUtil */ -require('~/graphs/stat_graph_contributors_util'); +import ContributorsStatGraphUtil from '~/graphs/stat_graph_contributors_util'; describe("ContributorsStatGraphUtil", function () { describe("#parse_log", function () { diff --git a/spec/javascripts/graphs/stat_graph_spec.js b/spec/javascripts/graphs/stat_graph_spec.js deleted file mode 100644 index 876c23361bc..00000000000 --- a/spec/javascripts/graphs/stat_graph_spec.js +++ /dev/null @@ -1,20 +0,0 @@ -/* eslint-disable quotes */ -/* global StatGraph */ - -require('~/graphs/stat_graph'); - -describe("StatGraph", function () { - describe("#get_log", function () { - it("returns log", function () { - StatGraph.log = "test"; - expect(StatGraph.get_log()).toBe("test"); - }); - }); - - describe("#set_log", function () { - it("sets the log", function () { - StatGraph.set_log("test"); - expect(StatGraph.log).toBe("test"); - }); - }); -}); diff --git a/spec/javascripts/header_spec.js b/spec/javascripts/header_spec.js index cecebb0b038..46a27b8c98f 100644 --- a/spec/javascripts/header_spec.js +++ b/spec/javascripts/header_spec.js @@ -6,7 +6,7 @@ require('~/lib/utils/text_utility'); (function() { describe('Header', function() { var todosPendingCount = '.todos-pending-count'; - var fixtureTemplate = 'static/header.html.raw'; + var fixtureTemplate = 'issues/open-issue.html.raw'; function isTodosCountHidden() { return $(todosPendingCount).hasClass('hidden'); @@ -45,9 +45,9 @@ require('~/lib/utils/text_utility'); expect(isTodosCountHidden()).toEqual(false); }); - it('should add delimiter to todos-pending-count', function() { - expect($(todosPendingCount).text()).toEqual('1,000'); + it('should show 99+ for todos-pending-count', function() { + expect($(todosPendingCount).text()).toEqual('99+'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/helpers/class_spec_helper.js.es6 b/spec/javascripts/helpers/class_spec_helper.js.es6 index d3c37d39431..61db27a8fcc 100644 --- a/spec/javascripts/helpers/class_spec_helper.js.es6 +++ b/spec/javascripts/helpers/class_spec_helper.js.es6 @@ -7,3 +7,5 @@ class ClassSpecHelper { } window.ClassSpecHelper = ClassSpecHelper; + +module.exports = ClassSpecHelper; diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index 5b0b7aa7903..e7530f61385 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -105,6 +105,7 @@ require('~/issue'); expectIssueState(false); expect($btnClose).toHaveProp('disabled', false); + expect($('.issue_counter')).toHaveText(0); }); it('fails to close an issue with success:false', function() { @@ -121,6 +122,7 @@ require('~/issue'); expectIssueState(true); expect($btnClose).toHaveProp('disabled', false); expectErrorMessage(); + expect($('.issue_counter')).toHaveText(1); }); it('fails to closes an issue with HTTP error', function() { @@ -135,6 +137,7 @@ require('~/issue'); expectIssueState(true); expect($btnClose).toHaveProp('disabled', true); expectErrorMessage(); + expect($('.issue_counter')).toHaveText(1); }); }); @@ -159,6 +162,7 @@ require('~/issue'); expectIssueState(true); expect($btnReopen).toHaveProp('disabled', false); + expect($('.issue_counter')).toHaveText(1); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js.es6 b/spec/javascripts/lib/utils/common_utils_spec.js.es6 index 006ede21093..f4d3e77e515 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js.es6 +++ b/spec/javascripts/lib/utils/common_utils_spec.js.es6 @@ -108,6 +108,30 @@ require('~/lib/utils/common_utils'); }); }); + describe('gl.utils.parseIntPagination', () => { + it('should parse to integers all string values and return pagination object', () => { + const pagination = { + 'X-PER-PAGE': 10, + 'X-PAGE': 2, + 'X-TOTAL': 30, + 'X-TOTAL-PAGES': 3, + 'X-NEXT-PAGE': 3, + 'X-PREV-PAGE': 1, + }; + + const expectedPagination = { + perPage: 10, + page: 2, + total: 30, + totalPages: 3, + nextPage: 3, + previousPage: 1, + }; + + expect(gl.utils.parseIntPagination(pagination)).toEqual(expectedPagination); + }); + }); + describe('gl.utils.isMetaClick', () => { it('should identify meta click on Windows/Linux', () => { const e = { diff --git a/spec/javascripts/lib/utils/text_utility_spec.js.es6 b/spec/javascripts/lib/utils/text_utility_spec.js.es6 index 86ade66ec29..4200e943121 100644 --- a/spec/javascripts/lib/utils/text_utility_spec.js.es6 +++ b/spec/javascripts/lib/utils/text_utility_spec.js.es6 @@ -35,5 +35,76 @@ require('~/lib/utils/text_utility'); expect(gl.text.pluralize('test', 1)).toBe('test'); }); }); + + describe('gl.text.highCountTrim', () => { + it('returns 99+ for count >= 100', () => { + expect(gl.text.highCountTrim(105)).toBe('99+'); + expect(gl.text.highCountTrim(100)).toBe('99+'); + }); + + it('returns exact number for count < 100', () => { + expect(gl.text.highCountTrim(45)).toBe(45); + }); + }); + + describe('gl.text.insertText', () => { + let textArea; + + beforeAll(() => { + textArea = document.createElement('textarea'); + document.querySelector('body').appendChild(textArea); + }); + + afterAll(() => { + textArea.parentNode.removeChild(textArea); + }); + + describe('without selection', () => { + it('inserts the tag on an empty line', () => { + const initialValue = ''; + + textArea.value = initialValue; + textArea.selectionStart = 0; + textArea.selectionEnd = 0; + + gl.text.insertText(textArea, textArea.value, '*', null, '', false); + + expect(textArea.value).toEqual(`${initialValue}* `); + }); + + it('inserts the tag on a new line if the current one is not empty', () => { + const initialValue = 'some text'; + + textArea.value = initialValue; + textArea.setSelectionRange(initialValue.length, initialValue.length); + + gl.text.insertText(textArea, textArea.value, '*', null, '', false); + + expect(textArea.value).toEqual(`${initialValue}\n* `); + }); + + it('inserts the tag on the same line if the current line only contains spaces', () => { + const initialValue = ' '; + + textArea.value = initialValue; + textArea.setSelectionRange(initialValue.length, initialValue.length); + + gl.text.insertText(textArea, textArea.value, '*', null, '', false); + + expect(textArea.value).toEqual(`${initialValue}* `); + }); + + it('inserts the tag on the same line if the current line only contains tabs', () => { + const initialValue = '\t\t\t'; + + textArea.value = initialValue; + textArea.setSelectionRange(initialValue.length, initialValue.length); + + gl.text.insertText(textArea, textArea.value, '*', null, '', false); + + expect(textArea.value).toEqual(`${initialValue}* `); + }); + }); + }); }); })(); diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js index 8b196f7720f..a0b2ebc221b 100644 --- a/spec/javascripts/line_highlighter_spec.js +++ b/spec/javascripts/line_highlighter_spec.js @@ -227,4 +227,4 @@ require('~/line_highlighter'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js index 25cfa9e9479..fd97dced870 100644 --- a/spec/javascripts/merge_request_spec.js +++ b/spec/javascripts/merge_request_spec.js @@ -6,9 +6,9 @@ require('~/merge_request'); (function() { describe('MergeRequest', function() { return describe('task lists', function() { - preloadFixtures('static/merge_requests_show.html.raw'); + preloadFixtures('merge_requests/merge_request_with_task_list.html.raw'); beforeEach(function() { - loadFixtures('static/merge_requests_show.html.raw'); + loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); return this.merge = new MergeRequest(); }); it('modifies the Markdown field', function() { @@ -19,11 +19,11 @@ require('~/merge_request'); return it('submits an ajax request on tasklist:changed', function() { spyOn(jQuery, 'ajax').and.callFake(function(req) { expect(req.type).toBe('PATCH'); - expect(req.url).toBe('/foo'); + expect(req.url).toBe(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`); return expect(req.data.merge_request.description).not.toBe(null); }); return $('.js-task-list-field').trigger('tasklist:changed'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 92a0f1c05f7..7506e6ab49e 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -25,7 +25,7 @@ require('vendor/jquery.scrollTo'); }; $.extend(stubLocation, defaults, stubs || {}); }; - preloadFixtures('static/merge_request_tabs.html.raw'); + preloadFixtures('merge_requests/merge_request_with_task_list.html.raw'); beforeEach(function () { this.class = new gl.MergeRequestTabs({ stubLocation: stubLocation }); @@ -41,7 +41,7 @@ require('vendor/jquery.scrollTo'); describe('#activateTab', function () { beforeEach(function () { spyOn($, 'ajax').and.callFake(function () {}); - loadFixtures('static/merge_request_tabs.html.raw'); + loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); this.subject = this.class.activateTab; }); it('shows the first tab when action is show', function () { @@ -62,19 +62,47 @@ require('vendor/jquery.scrollTo'); }); }); describe('#opensInNewTab', function () { - var commitsLink; var tabUrl; + var windowTarget = '_blank'; beforeEach(function () { - commitsLink = '.commits-tab li a'; - tabUrl = $(commitsLink).attr('href'); + loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); + + tabUrl = $('.commits-tab a').attr('href'); spyOn($.fn, 'attr').and.returnValue(tabUrl); }); + + describe('meta click', () => { + beforeEach(function () { + spyOn(gl.utils, 'isMetaClick').and.returnValue(true); + }); + + it('opens page when commits link is clicked', function () { + spyOn(window, 'open').and.callFake(function (url, name) { + expect(url).toEqual(tabUrl); + expect(name).toEqual(windowTarget); + }); + + this.class.bindEvents(); + document.querySelector('.merge-request-tabs .commits-tab a').click(); + }); + + it('opens page when commits badge is clicked', function () { + spyOn(window, 'open').and.callFake(function (url, name) { + expect(url).toEqual(tabUrl); + expect(name).toEqual(windowTarget); + }); + + this.class.bindEvents(); + document.querySelector('.merge-request-tabs .commits-tab a .badge').click(); + }); + }); + it('opens page tab in a new browser tab with Ctrl+Click - Windows/Linux', function () { spyOn(window, 'open').and.callFake(function (url, name) { expect(url).toEqual(tabUrl); - expect(name).toEqual('_blank'); + expect(name).toEqual(windowTarget); }); this.class.clickTab({ @@ -87,7 +115,7 @@ require('vendor/jquery.scrollTo'); it('opens page tab in a new browser tab with Cmd+Click - Mac', function () { spyOn(window, 'open').and.callFake(function (url, name) { expect(url).toEqual(tabUrl); - expect(name).toEqual('_blank'); + expect(name).toEqual(windowTarget); }); this.class.clickTab({ @@ -100,7 +128,7 @@ require('vendor/jquery.scrollTo'); it('opens page tab in a new browser tab with Middle-click - Mac/PC', function () { spyOn(window, 'open').and.callFake(function (url, name) { expect(url).toEqual(tabUrl); - expect(name).toEqual('_blank'); + expect(name).toEqual(windowTarget); }); this.class.clickTab({ @@ -181,4 +209,4 @@ require('vendor/jquery.scrollTo'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js index 8cefdd2409d..d5193b41c33 100644 --- a/spec/javascripts/merge_request_widget_spec.js +++ b/spec/javascripts/merge_request_widget_spec.js @@ -189,4 +189,4 @@ require('~/lib/utils/datetime_utility'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js index 9b657868523..90a429beeca 100644 --- a/spec/javascripts/new_branch_spec.js +++ b/spec/javascripts/new_branch_spec.js @@ -1,14 +1,13 @@ /* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, quotes, max-len */ /* global NewBranchForm */ -require('jquery-ui/ui/autocomplete'); require('~/new_branch_form'); (function() { describe('Branch', function() { return describe('create a new branch', function() { var expectToHaveError, fillNameWith; - preloadFixtures('static/new_branch.html.raw'); + preloadFixtures('branches/new_branch.html.raw'); fillNameWith = function(value) { return $('.js-branch-name').val(value).trigger('blur'); }; @@ -16,7 +15,7 @@ require('~/new_branch_form'); return expect($('.js-branch-name-error span').text()).toEqual(error); }; beforeEach(function() { - loadFixtures('static/new_branch.html.raw'); + loadFixtures('branches/new_branch.html.raw'); $('form').on('submit', function(e) { return e.preventDefault(); }); @@ -166,4 +165,4 @@ require('~/new_branch_form'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index af495787c54..d81a5bbb6a5 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -35,15 +35,13 @@ require('~/lib/utils/text_utility'); expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); }); - it('submits the form on tasklist:changed', function() { - var submitted = false; - $('form').on('submit', function(e) { - submitted = true; - e.preventDefault(); + it('submits an ajax request on tasklist:changed', function() { + spyOn(jQuery, 'ajax').and.callFake(function(req) { + expect(req.type).toBe('PATCH'); + expect(req.url).toBe('http://test.host/frontend-fixtures/issues-project/notes/1'); + return expect(req.data.note).not.toBe(null); }); - $('.js-task-list-field').trigger('tasklist:changed'); - expect(submitted).toBe(true); }); }); @@ -75,4 +73,4 @@ require('~/lib/utils/text_utility'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/project_dashboard_spec.js.es6 b/spec/javascripts/project_dashboard_spec.js.es6 deleted file mode 100644 index 24833b4eb57..00000000000 --- a/spec/javascripts/project_dashboard_spec.js.es6 +++ /dev/null @@ -1,86 +0,0 @@ -require('~/sidebar'); - -(() => { - describe('Project dashboard page', () => { - let $pageWithSidebar = null; - let $sidebarToggle = null; - let sidebar = null; - const fixtureTemplate = 'projects/dashboard.html.raw'; - - const assertSidebarStateExpanded = (shouldBeExpanded) => { - expect(sidebar.isExpanded).toBe(shouldBeExpanded); - expect($pageWithSidebar.hasClass('page-sidebar-expanded')).toBe(shouldBeExpanded); - }; - - preloadFixtures(fixtureTemplate); - beforeEach(() => { - loadFixtures(fixtureTemplate); - - $pageWithSidebar = $('.page-with-sidebar'); - $sidebarToggle = $('.toggle-nav-collapse'); - - // otherwise instantiating the Sidebar for the second time - // won't do anything, as the Sidebar is a singleton class - gl.Sidebar.singleton = null; - sidebar = new gl.Sidebar(); - }); - - it('can show the sidebar when the toggler is clicked', () => { - assertSidebarStateExpanded(false); - $sidebarToggle.click(); - assertSidebarStateExpanded(true); - }); - - it('should dismiss the sidebar when clone button clicked', () => { - $sidebarToggle.click(); - assertSidebarStateExpanded(true); - - const cloneButton = $('.project-clone-holder a.clone-dropdown-btn'); - cloneButton.click(); - assertSidebarStateExpanded(false); - }); - - it('should dismiss the sidebar when download button clicked', () => { - $sidebarToggle.click(); - assertSidebarStateExpanded(true); - - const downloadButton = $('.project-action-button .btn:has(i.fa-download)'); - downloadButton.click(); - assertSidebarStateExpanded(false); - }); - - it('should dismiss the sidebar when add button clicked', () => { - $sidebarToggle.click(); - assertSidebarStateExpanded(true); - - const addButton = $('.project-action-button .btn:has(i.fa-plus)'); - addButton.click(); - assertSidebarStateExpanded(false); - }); - - it('should dismiss the sidebar when notification button clicked', () => { - $sidebarToggle.click(); - assertSidebarStateExpanded(true); - - const notifButton = $('.js-notification-toggle-btns .notifications-btn'); - notifButton.click(); - assertSidebarStateExpanded(false); - }); - - it('should dismiss the sidebar when clicking on the body', () => { - $sidebarToggle.click(); - assertSidebarStateExpanded(true); - - $('body').click(); - assertSidebarStateExpanded(false); - }); - - it('should dismiss the sidebar when clicking on the project description header', () => { - $sidebarToggle.click(); - assertSidebarStateExpanded(true); - - $('.project-home-panel').click(); - assertSidebarStateExpanded(false); - }); - }); -})(); diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js index bfe3d2df79d..69d9587771f 100644 --- a/spec/javascripts/project_title_spec.js +++ b/spec/javascripts/project_title_spec.js @@ -10,11 +10,11 @@ require('~/project'); (function() { describe('Project Title', function() { - preloadFixtures('static/project_title.html.raw'); + preloadFixtures('issues/open-issue.html.raw'); loadJSONFixtures('projects.json'); beforeEach(function() { - loadFixtures('static/project_title.html.raw'); + loadFixtures('issues/open-issue.html.raw'); window.gon = {}; window.gon.api_version = 'v3'; @@ -38,15 +38,12 @@ require('~/project'); return spyOn(jQuery, 'ajax').and.callFake(fakeAjaxResponse.bind(_this)); }; })(this)); - it('to show on toggle click', (function(_this) { - return function() { - $('.js-projects-dropdown-toggle').click(); - return expect($('.header-content').hasClass('open')).toBe(true); - }; - })(this)); - return it('hide dropdown', function() { - $(".dropdown-menu-close-icon").click(); - return expect($('.header-content').hasClass('open')).toBe(false); + it('toggles dropdown', function() { + var menu = $('.js-dropdown-menu-projects'); + $('.js-projects-dropdown-toggle').click(); + expect(menu).toHaveClass('open'); + menu.find('.dropdown-menu-close-icon').click(); + expect(menu).not.toHaveClass('open'); }); }); @@ -54,4 +51,4 @@ require('~/project'); window.gon = {}; }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index f7636865aa1..4ac7e911740 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -34,7 +34,7 @@ require('~/extensions/jquery.js'); describe('RightSidebar', function() { var fixtureName = 'issues/open-issue.html.raw'; preloadFixtures(fixtureName); - loadJSONFixtures('todos.json'); + loadJSONFixtures('todos/todos.json'); beforeEach(function() { loadFixtures(fixtureName); @@ -64,7 +64,7 @@ require('~/extensions/jquery.js'); }); it('should broadcast todo:toggle event when add todo clicked', function() { - var todos = getJSONFixture('todos.json'); + var todos = getJSONFixture('todos/todos.json'); spyOn(jQuery, 'ajax').and.callFake(function() { var d = $.Deferred(); var response = todos; @@ -79,4 +79,4 @@ require('~/extensions/jquery.js'); expect(todoToggleSpy.calls.count()).toEqual(1); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index 9572b52ec1e..aaf058bd755 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -89,8 +89,8 @@ require('vendor/fuzzaldrin-plus'); var a1, a2, a3, a4, issuesAssignedToMeLink, issuesIHaveCreatedLink, mrsAssignedToMeLink, mrsIHaveCreatedLink; issuesAssignedToMeLink = issuesPath + "/?assignee_username=" + userName; issuesIHaveCreatedLink = issuesPath + "/?author_username=" + userName; - mrsAssignedToMeLink = mrsPath + "/?assignee_id=" + userId; - mrsIHaveCreatedLink = mrsPath + "/?author_id=" + userId; + mrsAssignedToMeLink = mrsPath + "/?assignee_username=" + userName; + mrsIHaveCreatedLink = mrsPath + "/?author_username=" + userName; a1 = "a[href='" + issuesAssignedToMeLink + "']"; a2 = "a[href='" + issuesIHaveCreatedLink + "']"; a3 = "a[href='" + mrsAssignedToMeLink + "']"; @@ -175,4 +175,4 @@ require('vendor/fuzzaldrin-plus'); expect(enterKeyEvent.isDefaultPrevented()).toBe(true); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index 602ac01aec3..ffff643e371 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -79,4 +79,4 @@ require('~/shortcuts_issuable'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/syntax_highlight_spec.js b/spec/javascripts/syntax_highlight_spec.js index c0c3837d1f4..cea223bd243 100644 --- a/spec/javascripts/syntax_highlight_spec.js +++ b/spec/javascripts/syntax_highlight_spec.js @@ -41,4 +41,4 @@ require('~/syntax_highlight'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index 7df8d2fd8b4..fae462561e9 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -5,23 +5,12 @@ jasmine.getFixtures().fixturesPath = 'base/spec/javascripts/fixtures'; jasmine.getJSONFixtures().fixturesPath = 'base/spec/javascripts/fixtures'; // include common libraries +require('~/commons/index.js'); window.$ = window.jQuery = require('jquery'); window._ = require('underscore'); -window.Cookies = require('vendor/js.cookie'); +window.Cookies = require('js-cookie'); window.Vue = require('vue'); window.Vue.use(require('vue-resource')); -require('jquery-ujs'); -require('bootstrap/js/affix'); -require('bootstrap/js/alert'); -require('bootstrap/js/button'); -require('bootstrap/js/collapse'); -require('bootstrap/js/dropdown'); -require('bootstrap/js/modal'); -require('bootstrap/js/scrollspy'); -require('bootstrap/js/tab'); -require('bootstrap/js/transition'); -require('bootstrap/js/tooltip'); -require('bootstrap/js/popover'); // stub expected globals window.gl = window.gl || {}; @@ -42,3 +31,38 @@ testsContext.keys().forEach(function (path) { }); } }); + +// workaround: include all source files to find files with 0% coverage +// see also https://github.com/deepsweet/istanbul-instrumenter-loader/issues/15 +describe('Uncovered files', function () { + // the following files throw errors because of undefined variables + const troubleMakers = [ + './blob_edit/blob_edit_bundle.js', + './cycle_analytics/components/stage_plan_component.js', + './cycle_analytics/components/stage_staging_component.js', + './cycle_analytics/components/stage_test_component.js', + './diff_notes/components/jump_to_discussion.js', + './diff_notes/components/resolve_count.js', + './merge_conflicts/components/inline_conflict_lines.js', + './merge_conflicts/components/parallel_conflict_lines.js', + './network/branch_graph.js', + ]; + + const sourceFiles = require.context('~', true, /^\.\/(?!application\.js).*\.(js|es6)$/); + sourceFiles.keys().forEach(function (path) { + // ignore if there is a matching spec file + if (testsContext.keys().indexOf(`${path.replace(/\.js(\.es6)?$/, '')}_spec`) > -1) { + return; + } + + it(`includes '${path}'`, function () { + try { + sourceFiles(path); + } catch (err) { + if (troubleMakers.indexOf(path) === -1) { + expect(err).toBeNull(); + } + } + }); + }); +}); diff --git a/spec/javascripts/todos_spec.js b/spec/javascripts/todos_spec.js new file mode 100644 index 00000000000..66e4fbd6304 --- /dev/null +++ b/spec/javascripts/todos_spec.js @@ -0,0 +1,63 @@ +require('~/todos'); +require('~/lib/utils/common_utils'); + +describe('Todos', () => { + preloadFixtures('todos/todos.html.raw'); + let todoItem; + + beforeEach(() => { + loadFixtures('todos/todos.html.raw'); + todoItem = document.querySelector('.todos-list .todo'); + + return new gl.Todos(); + }); + + describe('goToTodoUrl', () => { + it('opens the todo url', (done) => { + const todoLink = todoItem.dataset.url; + + spyOn(gl.utils, 'visitUrl').and.callFake((url) => { + expect(url).toEqual(todoLink); + done(); + }); + + todoItem.click(); + }); + + describe('meta click', () => { + let visitUrlSpy; + + beforeEach(() => { + spyOn(gl.utils, 'isMetaClick').and.returnValue(true); + visitUrlSpy = spyOn(gl.utils, 'visitUrl').and.callFake(() => {}); + }); + + it('opens the todo url in another tab', (done) => { + const todoLink = todoItem.dataset.url; + + spyOn(window, 'open').and.callFake((url, target) => { + expect(todoLink).toEqual(url); + expect(target).toEqual('_blank'); + done(); + }); + + todoItem.click(); + expect(visitUrlSpy).not.toHaveBeenCalled(); + }); + + it('opens the avatar\'s url in another tab when the avatar is clicked', (done) => { + const avatarImage = todoItem.querySelector('img'); + const avatarUrl = avatarImage.parentElement.getAttribute('href'); + + spyOn(window, 'open').and.callFake((url, target) => { + expect(avatarUrl).toEqual(url); + expect(target).toEqual('_blank'); + done(); + }); + + avatarImage.click(); + expect(visitUrlSpy).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js index cba1af4daa4..af2d02b6b29 100644 --- a/spec/javascripts/u2f/authenticate_spec.js +++ b/spec/javascripts/u2f/authenticate_spec.js @@ -69,4 +69,4 @@ require('./mock_u2f_device'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/u2f/mock_u2f_device.js b/spec/javascripts/u2f/mock_u2f_device.js index 287bfb4138b..6677fe9c1ee 100644 --- a/spec/javascripts/u2f/mock_u2f_device.js +++ b/spec/javascripts/u2f/mock_u2f_device.js @@ -30,4 +30,4 @@ return MockU2FDevice; })(); -}).call(this); +}).call(window); diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js index 10578c2c4b5..0f390c8b980 100644 --- a/spec/javascripts/u2f/register_spec.js +++ b/spec/javascripts/u2f/register_spec.js @@ -74,4 +74,4 @@ require('./mock_u2f_device'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/user_callout_spec.js.es6 b/spec/javascripts/user_callout_spec.js.es6 new file mode 100644 index 00000000000..205e72af600 --- /dev/null +++ b/spec/javascripts/user_callout_spec.js.es6 @@ -0,0 +1,57 @@ +const UserCallout = require('~/user_callout'); + +const USER_CALLOUT_COOKIE = 'user_callout_dismissed'; +const Cookie = window.Cookies; + +describe('UserCallout', function () { + const fixtureName = 'static/user_callout.html.raw'; + preloadFixtures(fixtureName); + + beforeEach(() => { + loadFixtures(fixtureName); + Cookie.remove(USER_CALLOUT_COOKIE); + + this.userCallout = new UserCallout(); + this.closeButton = $('.close-user-callout'); + this.userCalloutBtn = $('.user-callout-btn'); + this.userCalloutContainer = $('.user-callout'); + }); + + it('does not show when cookie is set not defined', () => { + expect(Cookie.get(USER_CALLOUT_COOKIE)).toBeUndefined(); + expect(this.userCalloutContainer.is(':visible')).toBe(true); + }); + + it('shows when cookie is set to false', () => { + Cookie.set(USER_CALLOUT_COOKIE, 'false'); + + expect(Cookie.get(USER_CALLOUT_COOKIE)).toBeDefined(); + expect(this.userCalloutContainer.is(':visible')).toBe(true); + }); + + it('hides when user clicks on the dismiss-icon', () => { + this.closeButton.click(); + expect(Cookie.get(USER_CALLOUT_COOKIE)).toBe('true'); + }); + + it('hides when user clicks on the "check it out" button', () => { + this.userCalloutBtn.click(); + expect(Cookie.get(USER_CALLOUT_COOKIE)).toBe('true'); + }); +}); + +describe('UserCallout when cookie is present', function () { + const fixtureName = 'static/user_callout.html.raw'; + preloadFixtures(fixtureName); + + beforeEach(() => { + loadFixtures(fixtureName); + Cookie.set(USER_CALLOUT_COOKIE, 'true'); + this.userCallout = new UserCallout(); + this.userCalloutContainer = $('.user-callout'); + }); + + it('removes the DOM element', () => { + expect(this.userCalloutContainer.length).toBe(0); + }); +}); diff --git a/spec/javascripts/version_check_image_spec.js.es6 b/spec/javascripts/version_check_image_spec.js.es6 new file mode 100644 index 00000000000..464c1fce210 --- /dev/null +++ b/spec/javascripts/version_check_image_spec.js.es6 @@ -0,0 +1,33 @@ +const ClassSpecHelper = require('./helpers/class_spec_helper'); +const VersionCheckImage = require('~/version_check_image'); +require('jquery'); + +describe('VersionCheckImage', function () { + describe('.bindErrorEvent', function () { + ClassSpecHelper.itShouldBeAStaticMethod(VersionCheckImage, 'bindErrorEvent'); + + beforeEach(function () { + this.imageElement = $('<div></div>'); + }); + + it('registers an error event', function () { + spyOn($.prototype, 'on'); + spyOn($.prototype, 'off').and.callFake(function () { return this; }); + + VersionCheckImage.bindErrorEvent(this.imageElement); + + expect($.prototype.off).toHaveBeenCalledWith('error'); + expect($.prototype.on).toHaveBeenCalledWith('error', jasmine.any(Function)); + }); + + it('hides the imageElement on error', function () { + spyOn($.prototype, 'hide'); + + VersionCheckImage.bindErrorEvent(this.imageElement); + + this.imageElement.trigger('error'); + + expect($.prototype.hide).toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 b/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 index e84f0dcfe67..9cb067921a7 100644 --- a/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 +++ b/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 @@ -6,12 +6,10 @@ describe('Pagination component', () => { const changeChanges = { one: '', - two: '', }; - const change = (one, two) => { + const change = (one) => { changeChanges.one = one; - changeChanges.two = two; }; it('should render and start at page 1', () => { @@ -34,7 +32,6 @@ describe('Pagination component', () => { component.changePage({ target: { innerText: '1' } }); expect(changeChanges.one).toEqual(1); - expect(changeChanges.two).toEqual('all'); }); it('should go to the previous page', () => { @@ -55,7 +52,6 @@ describe('Pagination component', () => { component.changePage({ target: { innerText: 'Prev' } }); expect(changeChanges.one).toEqual(1); - expect(changeChanges.two).toEqual('all'); }); it('should go to the next page', () => { @@ -76,7 +72,6 @@ describe('Pagination component', () => { component.changePage({ target: { innerText: 'Next' } }); expect(changeChanges.one).toEqual(5); - expect(changeChanges.two).toEqual('all'); }); it('should go to the last page', () => { @@ -97,7 +92,6 @@ describe('Pagination component', () => { component.changePage({ target: { innerText: 'Last >>' } }); expect(changeChanges.one).toEqual(10); - expect(changeChanges.two).toEqual('all'); }); it('should go to the first page', () => { @@ -118,7 +112,6 @@ describe('Pagination component', () => { component.changePage({ target: { innerText: '<< First' } }); expect(changeChanges.one).toEqual(1); - expect(changeChanges.two).toEqual('all'); }); it('should do nothing', () => { @@ -139,7 +132,6 @@ describe('Pagination component', () => { component.changePage({ target: { innerText: '...' } }); expect(changeChanges.one).toEqual(1); - expect(changeChanges.two).toEqual('all'); }); }); diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js index ce33a6814aa..99515f2e5f2 100644 --- a/spec/javascripts/zen_mode_spec.js +++ b/spec/javascripts/zen_mode_spec.js @@ -76,4 +76,4 @@ require('~/zen_mode'); keyCode: 27 })); }; -}).call(this); +}).call(window); diff --git a/spec/lib/additional_email_headers_interceptor_spec.rb b/spec/lib/additional_email_headers_interceptor_spec.rb new file mode 100644 index 00000000000..580450eef1e --- /dev/null +++ b/spec/lib/additional_email_headers_interceptor_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe AdditionalEmailHeadersInterceptor do + it 'adds Auto-Submitted header' do + mail = ActionMailer::Base.mail(to: 'test@mail.com', from: 'info@mail.com', body: 'hello').deliver + + expect(mail.header['To'].value).to eq('test@mail.com') + expect(mail.header['From'].value).to eq('info@mail.com') + expect(mail.header['Auto-Submitted'].value).to eq('auto-generated') + expect(mail.header['X-Auto-Response-Suppress'].value).to eq('All') + end +end diff --git a/spec/lib/banzai/filter/image_link_filter_spec.rb b/spec/lib/banzai/filter/image_link_filter_spec.rb index a2a1ed58d1b..294558b3db2 100644 --- a/spec/lib/banzai/filter/image_link_filter_spec.rb +++ b/spec/lib/banzai/filter/image_link_filter_spec.rb @@ -13,8 +13,8 @@ describe Banzai::Filter::ImageLinkFilter, lib: true do end it 'does not wrap a duplicate link' do - exp = act = %q(<a href="/whatever">#{image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')}</a>) - expect(filter(act).to_html).to eq exp + doc = filter(%Q(<a href="/whatever">#{image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')}</a>)) + expect(doc.to_html).to match /^<a href="\/whatever"><img[^>]*><\/a>$/ end it 'works with external images' do @@ -22,8 +22,8 @@ describe Banzai::Filter::ImageLinkFilter, lib: true do expect(doc.at_css('img')['src']).to eq doc.at_css('a')['href'] end - it 'wraps the image with a link and a div' do - doc = filter(image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) - expect(doc.to_html).to include('<div class="image-container">') + it 'works with inline images' do + doc = filter(%Q(<p>test #{image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')} inline</p>)) + expect(doc.to_html).to match /^<p>test <a[^>]*><img[^>]*><\/a> inline<\/p>$/ end end diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb index 456dbac0698..11607d4fb26 100644 --- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb @@ -311,7 +311,7 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do end end - describe '#issues_per_Project' do + describe '#issues_per_project' do context 'using an internal issue tracker' do it 'returns a Hash containing the issues per project' do doc = Nokogiri::HTML.fragment('') @@ -346,4 +346,26 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do end end end + + describe '.references_in' do + let(:merge_request) { create(:merge_request) } + + it 'yields valid references' do + expect do |b| + described_class.references_in(issue.to_reference, &b) + end.to yield_with_args(issue.to_reference, issue.iid, nil, nil, MatchData) + end + + it "doesn't yield invalid references" do + expect do |b| + described_class.references_in('#0', &b) + end.not_to yield_control + end + + it "doesn't yield unsupported references" do + expect do |b| + described_class.references_in(merge_request.to_reference, &b) + end.not_to yield_control + end + end end diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb index 3e1ac9fb2b2..9873774909e 100644 --- a/spec/lib/banzai/filter/user_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb @@ -112,6 +112,25 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do end end + context 'mentioning a nested group' do + it_behaves_like 'a reference containing an element node' + + let(:group) { create(:group, :nested) } + let(:reference) { group.to_reference } + + it 'links to the nested group' do + doc = reference_filter("Hey #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls.group_url(group) + end + + it 'has the full group name as a title' do + doc = reference_filter("Hey #{reference}") + + expect(doc.css('a').first.attr('title')).to eq group.full_name + end + end + it 'links with adjacent text' do doc = reference_filter("Mention me (#{reference}.)") expect(doc.to_html).to match(/\(<a.+>#{reference}<\/a>\.\)/) diff --git a/spec/lib/bitbucket/collection_spec.rb b/spec/lib/bitbucket/collection_spec.rb index 015a7f80e03..9008cb3e870 100644 --- a/spec/lib/bitbucket/collection_spec.rb +++ b/spec/lib/bitbucket/collection_spec.rb @@ -19,6 +19,6 @@ describe Bitbucket::Collection do it "iterates paginator" do collection = described_class.new(TestPaginator.new) - expect(collection.to_a).to match(["result_1_page_1", "result_2_page_1", "result_1_page_2", "result_2_page_2"]) + expect(collection.to_a).to match(%w(result_1_page_1 result_2_page_1 result_1_page_2 result_2_page_2)) end end diff --git a/spec/lib/bitbucket/representation/repo_spec.rb b/spec/lib/bitbucket/representation/repo_spec.rb index adcd978e1b3..405265cc669 100644 --- a/spec/lib/bitbucket/representation/repo_spec.rb +++ b/spec/lib/bitbucket/representation/repo_spec.rb @@ -29,7 +29,7 @@ describe Bitbucket::Representation::Repo do end describe '#owner_and_slug' do - it { expect(described_class.new({ 'full_name' => 'ben/test' }).owner_and_slug).to eq(['ben', 'test']) } + it { expect(described_class.new({ 'full_name' => 'ben/test' }).owner_and_slug).to eq(%w(ben test)) } end describe '#owner' do @@ -42,7 +42,7 @@ describe Bitbucket::Representation::Repo do describe '#clone_url' do it 'builds url' do - data = { 'links' => { 'clone' => [ { 'name' => 'https', 'href' => 'https://bibucket.org/test/test.git' }] } } + data = { 'links' => { 'clone' => [{ 'name' => 'https', 'href' => 'https://bibucket.org/test/test.git' }] } } expect(described_class.new(data).clone_url('abc')).to eq('https://x-token-auth:abc@bibucket.org/test/test.git') end end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 68ad429608d..7145f0da1d3 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -96,7 +96,7 @@ module Ci it "returns builds if only has a list of branches including specified" do config = YAML.dump({ before_script: ["pwd"], - rspec: { script: "rspec", type: type, only: ["master", "deploy"] } + rspec: { script: "rspec", type: type, only: %w(master deploy) } }) config_processor = GitlabCiYamlProcessor.new(config, path) @@ -173,8 +173,8 @@ module Ci it "returns build only for specified type" do config = YAML.dump({ before_script: ["pwd"], - rspec: { script: "rspec", type: "test", only: ["master", "deploy"] }, - staging: { script: "deploy", type: "deploy", only: ["master", "deploy"] }, + rspec: { script: "rspec", type: "test", only: %w(master deploy) }, + staging: { script: "deploy", type: "deploy", only: %w(master deploy) }, production: { script: "deploy", type: "deploy", only: ["master@path", "deploy"] }, }) @@ -252,7 +252,7 @@ module Ci it "does not return builds if except has a list of branches including specified" do config = YAML.dump({ before_script: ["pwd"], - rspec: { script: "rspec", type: type, except: ["master", "deploy"] } + rspec: { script: "rspec", type: type, except: %w(master deploy) } }) config_processor = GitlabCiYamlProcessor.new(config, path) @@ -580,7 +580,7 @@ module Ci context 'when syntax is incorrect' do context 'when variables defined but invalid' do let(:variables) do - [ 'VAR1', 'value1', 'VAR2', 'value2' ] + %w(VAR1 value1 VAR2 value2) end it 'raises error' do @@ -909,7 +909,7 @@ module Ci end context 'dependencies to builds' do - let(:dependencies) { ['build1', 'build2'] } + let(:dependencies) { %w(build1 build2) } it { expect { subject }.not_to raise_error } end @@ -1215,7 +1215,7 @@ EOT end it "returns errors if job stage is not a defined stage" do - config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", type: "acceptance" } }) + config = YAML.dump({ types: %w(build test), rspec: { script: "test", type: "acceptance" } }) expect do GitlabCiYamlProcessor.new(config, path) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test") @@ -1257,42 +1257,42 @@ EOT end it "returns errors if job artifacts:name is not an a string" do - config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { name: 1 } } }) + config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { name: 1 } } }) expect do GitlabCiYamlProcessor.new(config) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts name should be a string") end it "returns errors if job artifacts:when is not an a predefined value" do - config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { when: 1 } } }) + config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { when: 1 } } }) expect do GitlabCiYamlProcessor.new(config) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts when should be on_success, on_failure or always") end it "returns errors if job artifacts:expire_in is not an a string" do - config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { expire_in: 1 } } }) + config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: 1 } } }) expect do GitlabCiYamlProcessor.new(config) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration") end it "returns errors if job artifacts:expire_in is not an a valid duration" do - config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { expire_in: "7 elephants" } } }) + config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: "7 elephants" } } }) expect do GitlabCiYamlProcessor.new(config) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration") end it "returns errors if job artifacts:untracked is not an array of strings" do - config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { untracked: "string" } } }) + config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { untracked: "string" } } }) expect do GitlabCiYamlProcessor.new(config) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts untracked should be a boolean value") end it "returns errors if job artifacts:paths is not an array of strings" do - config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { paths: "string" } } }) + config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { paths: "string" } } }) expect do GitlabCiYamlProcessor.new(config) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts paths should be an array of strings") @@ -1320,28 +1320,28 @@ EOT end it "returns errors if job cache:key is not an a string" do - config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { key: 1 } } }) + config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: 1 } } }) expect do GitlabCiYamlProcessor.new(config) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:key config should be a string or symbol") end it "returns errors if job cache:untracked is not an array of strings" do - config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { untracked: "string" } } }) + config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { untracked: "string" } } }) expect do GitlabCiYamlProcessor.new(config) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:untracked config should be a boolean value") end it "returns errors if job cache:paths is not an array of strings" do - config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { paths: "string" } } }) + config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { paths: "string" } } }) expect do GitlabCiYamlProcessor.new(config) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:paths config should be an array of strings") end it "returns errors if job dependencies is not an array of strings" do - config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", dependencies: "string" } }) + config = YAML.dump({ types: %w(build test), rspec: { script: "test", dependencies: "string" } }) expect do GitlabCiYamlProcessor.new(config) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec dependencies should be an array of strings") diff --git a/spec/lib/constraints/project_url_constrainer_spec.rb b/spec/lib/constraints/project_url_constrainer_spec.rb index a5251e9a8c2..4f25ad88960 100644 --- a/spec/lib/constraints/project_url_constrainer_spec.rb +++ b/spec/lib/constraints/project_url_constrainer_spec.rb @@ -6,7 +6,7 @@ describe ProjectUrlConstrainer, lib: true do describe '#matches?' do context 'valid request' do - let(:request) { build_request(namespace.path, project.path) } + let(:request) { build_request(namespace.full_path, project.path) } it { expect(subject.matches?(request)).to be_truthy } end @@ -19,7 +19,7 @@ describe ProjectUrlConstrainer, lib: true do end context "project id ending with .git" do - let(:request) { build_request(namespace.path, project.path + '.git') } + let(:request) { build_request(namespace.full_path, project.path + '.git') } it { expect(subject.matches?(request)).to be_falsey } end diff --git a/spec/lib/expand_variables_spec.rb b/spec/lib/expand_variables_spec.rb index 90bc7dad379..730ca1f7c0a 100644 --- a/spec/lib/expand_variables_spec.rb +++ b/spec/lib/expand_variables_spec.rb @@ -7,58 +7,49 @@ describe ExpandVariables do tests = [ { value: 'key', result: 'key', - variables: [] - }, + variables: [] }, { value: 'key$variable', result: 'key', - variables: [] - }, + variables: [] }, { value: 'key$variable', result: 'keyvalue', variables: [ { key: 'variable', value: 'value' } - ] - }, + ] }, { value: 'key${variable}', result: 'keyvalue', variables: [ { key: 'variable', value: 'value' } - ] - }, + ] }, { value: 'key$variable$variable2', result: 'keyvalueresult', variables: [ { key: 'variable', value: 'value' }, { key: 'variable2', value: 'result' }, - ] - }, + ] }, { value: 'key${variable}${variable2}', result: 'keyvalueresult', variables: [ { key: 'variable', value: 'value' }, { key: 'variable2', value: 'result' } - ] - }, + ] }, { value: 'key$variable2$variable', result: 'keyresultvalue', variables: [ { key: 'variable', value: 'value' }, { key: 'variable2', value: 'result' }, - ] - }, + ] }, { value: 'key${variable2}${variable}', result: 'keyresultvalue', variables: [ { key: 'variable', value: 'value' }, { key: 'variable2', value: 'result' } - ] - }, + ] }, { value: 'review/$CI_BUILD_REF_NAME', result: 'review/feature/add-review-apps', variables: [ { key: 'CI_BUILD_REF_NAME', value: 'feature/add-review-apps' } - ] - }, + ] }, ] tests.each do |test| diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb index 29c07655ae8..33ab005667a 100644 --- a/spec/lib/extracts_path_spec.rb +++ b/spec/lib/extracts_path_spec.rb @@ -177,12 +177,12 @@ describe ExtractsPath, lib: true do it "extracts a valid commit SHA" do expect(extract_ref('f4b14494ef6abf3d144c28e4af0c20143383e062/CHANGELOG')).to eq( - ['f4b14494ef6abf3d144c28e4af0c20143383e062', 'CHANGELOG'] + %w(f4b14494ef6abf3d144c28e4af0c20143383e062 CHANGELOG) ) end it "falls back to a primitive split for an invalid ref" do - expect(extract_ref('stable/CHANGELOG')).to eq(['stable', 'CHANGELOG']) + expect(extract_ref('stable/CHANGELOG')).to eq(%w(stable CHANGELOG)) end end end diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb index ba199917f5c..bca57105d1d 100644 --- a/spec/lib/gitlab/asciidoc_spec.rb +++ b/spec/lib/gitlab/asciidoc_spec.rb @@ -41,6 +41,29 @@ module Gitlab render(input, context, asciidoc_opts) end end + + context "XSS" do + links = { + 'links' => { + input: 'link:mylink"onmouseover="alert(1)[Click Here]', + output: "<div>\n<p><a href=\"mylink\">Click Here</a></p>\n</div>" + }, + 'images' => { + input: 'image:https://localhost.com/image.png[Alt text" onerror="alert(7)]', + output: "<div>\n<p><span><img src=\"https://localhost.com/image.png\" alt=\"Alt text\"></span></p>\n</div>" + }, + 'pre' => { + input: '```mypre"><script>alert(3)</script>', + output: "<div>\n<div>\n<pre lang=\"mypre\">\"><code></code></pre>\n</div>\n</div>" + } + } + + links.each do |name, data| + it "does not convert dangerous #{name} into HTML" do + expect(render(data[:input], context)).to eql data[:output] + end + end + end end def render(*args) diff --git a/spec/lib/gitlab/badge/shared/metadata.rb b/spec/lib/gitlab/badge/shared/metadata.rb index 0cf18514251..63c7ca5a915 100644 --- a/spec/lib/gitlab/badge/shared/metadata.rb +++ b/spec/lib/gitlab/badge/shared/metadata.rb @@ -18,4 +18,14 @@ shared_examples 'badge metadata' do it { is_expected.to include metadata.image_url } it { is_expected.to include metadata.link_url } end + + describe '#to_asciidoc' do + subject { metadata.to_asciidoc } + + it { is_expected.to include metadata.image_url } + it { is_expected.to include metadata.link_url } + it { is_expected.to include 'image:' } + it { is_expected.to include 'link=' } + it { is_expected.to include 'title=' } + end end diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb index 0a2fe5af2c3..a7ee7f53a6b 100644 --- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb @@ -87,10 +87,10 @@ describe Gitlab::BitbucketImport::Importer, lib: true do body: issues_statuses_sample_data.to_json) stub_request(:get, "https://api.bitbucket.org/2.0/repositories/namespace/repo?pagelen=50&sort=created_on"). - with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer', 'User-Agent' => 'Faraday v0.9.2' }). - to_return(status: 200, - body: "", - headers: {}) + with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer', 'User-Agent' => 'Faraday v0.9.2' }). + to_return(status: 200, + body: "", + headers: {}) sample_issues_statuses.each_with_index do |issue, index| stub_request( diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb b/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb index 5b678d31fce..3916fc704a4 100644 --- a/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb +++ b/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb @@ -26,6 +26,21 @@ describe Gitlab::ChatCommands::Presenters::IssueShow do end end + context 'with labels' do + let(:label) { create(:label, project: project, title: 'mep') } + let(:label1) { create(:label, project: project, title: 'mop') } + + before do + issue.labels << [label, label1] + end + + it 'shows the labels' do + labels = attachment[:fields].find { |f| f[:title] == 'Labels' } + + expect(labels[:value]).to eq("mep, mop") + end + end + context 'confidential issue' do let(:issue) { create(:issue, project: project) } diff --git a/spec/lib/gitlab/ci/config/entry/commands_spec.rb b/spec/lib/gitlab/ci/config/entry/commands_spec.rb index b8b0825a1c7..afa4a089418 100644 --- a/spec/lib/gitlab/ci/config/entry/commands_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/commands_spec.rb @@ -4,7 +4,7 @@ describe Gitlab::Ci::Config::Entry::Commands do let(:entry) { described_class.new(config) } context 'when entry config value is an array' do - let(:config) { ['ls', 'pwd'] } + let(:config) { %w(ls pwd) } describe '#value' do it 'returns array of strings' do diff --git a/spec/lib/gitlab/ci/config/entry/factory_spec.rb b/spec/lib/gitlab/ci/config/entry/factory_spec.rb index 00dad5d9591..3395b3c645b 100644 --- a/spec/lib/gitlab/ci/config/entry/factory_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/factory_spec.rb @@ -8,20 +8,20 @@ describe Gitlab::Ci::Config::Entry::Factory do context 'when setting a concrete value' do it 'creates entry with valid value' do entry = factory - .value(['ls', 'pwd']) + .value(%w(ls pwd)) .create! - expect(entry.value).to eq ['ls', 'pwd'] + expect(entry.value).to eq %w(ls pwd) end context 'when setting description' do it 'creates entry with description' do entry = factory - .value(['ls', 'pwd']) + .value(%w(ls pwd)) .with(description: 'test description') .create! - expect(entry.value).to eq ['ls', 'pwd'] + expect(entry.value).to eq %w(ls pwd) expect(entry.description).to eq 'test description' end end @@ -29,7 +29,7 @@ describe Gitlab::Ci::Config::Entry::Factory do context 'when setting key' do it 'creates entry with custom key' do entry = factory - .value(['ls', 'pwd']) + .value(%w(ls pwd)) .with(key: 'test key') .create! diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb index 432a99dce33..ebd80ac5e1d 100644 --- a/spec/lib/gitlab/ci/config/entry/global_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb @@ -21,12 +21,12 @@ describe Gitlab::Ci::Config::Entry::Global do context 'when configuration is valid' do context 'when some entries defined' do let(:hash) do - { before_script: ['ls', 'pwd'], + { before_script: %w(ls pwd), image: 'ruby:2.2', services: ['postgres:9.1', 'mysql:5.5'], variables: { VAR: 'value' }, after_script: ['make clean'], - stages: ['build', 'pages'], + stages: %w(build pages), cache: { key: 'k', untracked: true, paths: ['public/'] }, rspec: { script: %w[rspec ls] }, spinach: { before_script: [], variables: {}, script: 'spinach' } } @@ -89,7 +89,7 @@ describe Gitlab::Ci::Config::Entry::Global do describe '#before_script_value' do it 'returns correct script' do - expect(global.before_script_value).to eq ['ls', 'pwd'] + expect(global.before_script_value).to eq %w(ls pwd) end end @@ -126,7 +126,7 @@ describe Gitlab::Ci::Config::Entry::Global do context 'when deprecated types key defined' do let(:hash) do - { types: ['test', 'deploy'], + { types: %w(test deploy), rspec: { script: 'rspec' } } end @@ -148,7 +148,7 @@ describe Gitlab::Ci::Config::Entry::Global do expect(global.jobs_value).to eq( rspec: { name: :rspec, script: %w[rspec ls], - before_script: ['ls', 'pwd'], + before_script: %w(ls pwd), commands: "ls\npwd\nrspec\nls", image: 'ruby:2.2', services: ['postgres:9.1', 'mysql:5.5'], diff --git a/spec/lib/gitlab/ci/config/entry/key_spec.rb b/spec/lib/gitlab/ci/config/entry/key_spec.rb index a55e5b4b8ac..0dd36fe1f44 100644 --- a/spec/lib/gitlab/ci/config/entry/key_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/key_spec.rb @@ -21,7 +21,7 @@ describe Gitlab::Ci::Config::Entry::Key do end context 'when entry value is not correct' do - let(:config) { [ 'incorrect' ] } + let(:config) { ['incorrect'] } describe '#errors' do it 'saves errors' do diff --git a/spec/lib/gitlab/ci/config/entry/paths_spec.rb b/spec/lib/gitlab/ci/config/entry/paths_spec.rb index e60c9aaf661..1d9c5ddee9b 100644 --- a/spec/lib/gitlab/ci/config/entry/paths_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/paths_spec.rb @@ -21,7 +21,7 @@ describe Gitlab::Ci::Config::Entry::Paths do end context 'when entry value is not valid' do - let(:config) { [ 1 ] } + let(:config) { [1] } describe '#errors' do it 'saves errors' do diff --git a/spec/lib/gitlab/ci/config/entry/script_spec.rb b/spec/lib/gitlab/ci/config/entry/script_spec.rb index aa99cee2690..069eaa26422 100644 --- a/spec/lib/gitlab/ci/config/entry/script_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/script_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::Ci::Config::Entry::Script do describe 'validations' do context 'when entry config value is correct' do - let(:config) { ['ls', 'pwd'] } + let(:config) { %w(ls pwd) } describe '#value' do it 'returns array of strings' do diff --git a/spec/lib/gitlab/ci/config/entry/variables_spec.rb b/spec/lib/gitlab/ci/config/entry/variables_spec.rb index 58327d08904..f15f02f403e 100644 --- a/spec/lib/gitlab/ci/config/entry/variables_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/variables_spec.rb @@ -29,7 +29,7 @@ describe Gitlab::Ci::Config::Entry::Variables do end context 'when entry value is not correct' do - let(:config) { [ :VAR, 'test' ] } + let(:config) { [:VAR, 'test'] } describe '#errors' do it 'saves errors' do diff --git a/spec/lib/gitlab/conflict/file_spec.rb b/spec/lib/gitlab/conflict/file_spec.rb index fbf679c5215..780ac0ad97e 100644 --- a/spec/lib/gitlab/conflict/file_spec.rb +++ b/spec/lib/gitlab/conflict/file_spec.rb @@ -44,7 +44,7 @@ describe Gitlab::Conflict::File, lib: true do it 'returns a file containing only the chosen parts of the resolved sections' do expect(resolved_lines.chunk { |line| line.type || 'both' }.map(&:first)). - to eq(['both', 'new', 'both', 'old', 'both', 'new', 'both']) + to eq(%w(both new both old both new both)) end end @@ -123,7 +123,7 @@ describe Gitlab::Conflict::File, lib: true do it 'sets conflict to true for sections with only changed lines' do conflict_file.sections.select { |section| section[:conflict] }.each do |section| section[:lines].each do |line| - expect(line.type).to be_in(['new', 'old']) + expect(line.type).to be_in(%w(new old)) end end end @@ -251,7 +251,7 @@ FILE describe '#as_json' do it 'includes the blob path for the file' do expect(conflict_file.as_json[:blob_path]). - to eq("/#{project.namespace.to_param}/#{merge_request.project.to_param}/blob/#{our_commit.oid}/files/ruby/regex.rb") + to eq("/#{project.full_path}/blob/#{our_commit.oid}/files/ruby/regex.rb") end it 'includes the blob icon for the file' do diff --git a/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb new file mode 100644 index 00000000000..c455cd9b942 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +describe Gitlab::CycleAnalytics::BaseEventFetcher do + let(:max_events) { 2 } + let(:project) { create(:project) } + let(:user) { create(:user, :admin) } + let(:start_time_attrs) { Issue.arel_table[:created_at] } + let(:end_time_attrs) { [Issue::Metrics.arel_table[:first_associated_with_milestone_at]] } + let(:options) do + { start_time_attrs: start_time_attrs, + end_time_attrs: end_time_attrs, + from: 30.days.ago } + end + + subject do + described_class.new(project: project, + stage: :issue, + options: options).fetch + end + + before do + allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return(Issue.all) + allow_any_instance_of(Gitlab::CycleAnalytics::BaseEventFetcher).to receive(:serialize) do |event| + event + end + + stub_const('Gitlab::CycleAnalytics::BaseEventFetcher::MAX_EVENTS', max_events) + + setup_events(count: 3) + end + + it 'limits the rows to the max number' do + expect(subject.count).to eq(max_events) + end + + def setup_events(count:) + count.times do + issue = create(:issue, project: project, created_at: 2.days.ago) + milestone = create(:milestone, project: project) + + issue.update(milestone: milestone) + create_merge_request_closing_issue(issue) + end + end +end diff --git a/spec/lib/gitlab/data_builder/build_spec.rb b/spec/lib/gitlab/data_builder/build_spec.rb index 6c71e98066b..91c43f2bdc0 100644 --- a/spec/lib/gitlab/data_builder/build_spec.rb +++ b/spec/lib/gitlab/data_builder/build_spec.rb @@ -17,5 +17,31 @@ describe Gitlab::DataBuilder::Build do it { expect(data[:build_allow_failure]).to eq(false) } it { expect(data[:project_id]).to eq(build.project.id) } it { expect(data[:project_name]).to eq(build.project.name_with_namespace) } + + context 'commit author_url' do + context 'when no commit present' do + let(:build) { create(:ci_build) } + + it 'sets to mailing address of git_author_email' do + expect(data[:commit][:author_url]).to eq("mailto:#{build.pipeline.git_author_email}") + end + end + + context 'when commit present but has no author' do + let(:build) { create(:ci_build, :with_commit) } + + it 'sets to mailing address of git_author_email' do + expect(data[:commit][:author_url]).to eq("mailto:#{build.pipeline.git_author_email}") + end + end + + context 'when commit and author are present' do + let(:build) { create(:ci_build, :with_commit_and_author) } + + it 'sets to GitLab user url' do + expect(data[:commit][:author_url]).to eq(Gitlab::Routing.url_helpers.user_url(username: build.commit.author.username)) + end + end + end end end diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index 7fd25b9e5bf..e007044868c 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -12,15 +12,14 @@ describe Gitlab::Database::MigrationHelpers, lib: true do describe '#add_concurrent_index' do context 'outside a transaction' do before do - expect(model).to receive(:transaction_open?).and_return(false) - - unless Gitlab::Database.postgresql? - allow_any_instance_of(Gitlab::Database::MigrationHelpers).to receive(:disable_statement_timeout) - end + allow(model).to receive(:transaction_open?).and_return(false) end context 'using PostgreSQL' do - before { expect(Gitlab::Database).to receive(:postgresql?).and_return(true) } + before do + allow(Gitlab::Database).to receive(:postgresql?).and_return(true) + allow(model).to receive(:disable_statement_timeout) + end it 'creates the index concurrently' do expect(model).to receive(:add_index). @@ -59,6 +58,81 @@ describe Gitlab::Database::MigrationHelpers, lib: true do end end + describe '#add_concurrent_foreign_key' do + context 'inside a transaction' do + it 'raises an error' do + expect(model).to receive(:transaction_open?).and_return(true) + + expect do + model.add_concurrent_foreign_key(:projects, :users, column: :user_id) + end.to raise_error(RuntimeError) + end + end + + context 'outside a transaction' do + before do + allow(model).to receive(:transaction_open?).and_return(false) + end + + context 'using MySQL' do + it 'creates a regular foreign key' do + allow(Gitlab::Database).to receive(:mysql?).and_return(true) + + expect(model).to receive(:add_foreign_key). + with(:projects, :users, column: :user_id, on_delete: :cascade) + + model.add_concurrent_foreign_key(:projects, :users, column: :user_id) + end + end + + context 'using PostgreSQL' do + before do + allow(Gitlab::Database).to receive(:mysql?).and_return(false) + end + + it 'creates a concurrent foreign key' do + expect(model).to receive(:disable_statement_timeout) + expect(model).to receive(:execute).ordered.with(/NOT VALID/) + expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/) + + model.add_concurrent_foreign_key(:projects, :users, column: :user_id) + end + end + end + end + + describe '#concurrent_foreign_key_name' do + it 'returns the name for a foreign key' do + name = model.concurrent_foreign_key_name(:this_is_a_very_long_table_name, + :with_a_very_long_column_name) + + expect(name).to be_an_instance_of(String) + expect(name.length).to eq(13) + end + end + + describe '#disable_statement_timeout' do + context 'using PostgreSQL' do + it 'disables statement timeouts' do + expect(Gitlab::Database).to receive(:postgresql?).and_return(true) + + expect(model).to receive(:execute).with('SET statement_timeout TO 0') + + model.disable_statement_timeout + end + end + + context 'using MySQL' do + it 'does nothing' do + expect(Gitlab::Database).to receive(:postgresql?).and_return(false) + + expect(model).not_to receive(:execute) + + model.disable_statement_timeout + end + end + end + describe '#update_column_in_batches' do before do create_list(:empty_project, 5) diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index b142b3a2781..edd01d032c8 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -5,6 +5,12 @@ class MigrationTest end describe Gitlab::Database, lib: true do + describe '.adapter_name' do + it 'returns the name of the adapter' do + expect(described_class.adapter_name).to be_an_instance_of(String) + end + end + # These are just simple smoke tests to check if the methods work (regardless # of what they may return). describe '.mysql?' do @@ -71,6 +77,69 @@ describe Gitlab::Database, lib: true do end end + describe '.with_connection_pool' do + it 'creates a new connection pool and disconnect it after used' do + closed_pool = nil + + described_class.with_connection_pool(1) do |pool| + pool.with_connection do |connection| + connection.execute('SELECT 1 AS value') + end + + expect(pool).to be_connected + + closed_pool = pool + end + + expect(closed_pool).not_to be_connected + end + + it 'disconnects the pool even an exception was raised' do + error = Class.new(RuntimeError) + closed_pool = nil + + begin + described_class.with_connection_pool(1) do |pool| + pool.with_connection do |connection| + connection.execute('SELECT 1 AS value') + end + + closed_pool = pool + + raise error.new('boom') + end + rescue error + end + + expect(closed_pool).not_to be_connected + end + end + + describe '.create_connection_pool' do + it 'creates a new connection pool with specific pool size' do + pool = described_class.create_connection_pool(5) + + begin + expect(pool) + .to be_kind_of(ActiveRecord::ConnectionAdapters::ConnectionPool) + + expect(pool.spec.config[:pool]).to eq(5) + ensure + pool.disconnect! + end + end + + it 'allows setting of a custom hostname' do + pool = described_class.create_connection_pool(5, '127.0.0.1') + + begin + expect(pool.spec.config[:host]).to eq('127.0.0.1') + ensure + pool.disconnect! + end + end + end + describe '#true_value' do it 'returns correct value for PostgreSQL' do expect(described_class).to receive(:postgresql?).and_return(true) diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb index 5893485634d..0e9309d278e 100644 --- a/spec/lib/gitlab/diff/highlight_spec.rb +++ b/spec/lib/gitlab/diff/highlight_spec.rb @@ -53,21 +53,21 @@ describe Gitlab::Diff::Highlight, lib: true do end it 'marks unchanged lines' do - code = %Q{ def popen(cmd, path=nil)} + code = %q{ def popen(cmd, path=nil)} expect(subject[2].text).to eq(code) expect(subject[2].text).not_to be_html_safe end it 'marks removed lines' do - code = %Q{- raise "System commands must be given as an array of strings"} + code = %q{- raise "System commands must be given as an array of strings"} expect(subject[4].text).to eq(code) expect(subject[4].text).not_to be_html_safe end it 'marks added lines' do - code = %Q{+ raise <span class='idiff left right'>RuntimeError, </span>"System commands must be given as an array of strings"} + code = %q{+ raise <span class='idiff left right'>RuntimeError, </span>"System commands must be given as an array of strings"} expect(subject[5].text).to eq(code) expect(subject[5].text).to be_html_safe diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb index 8e3e4034c8f..994995b57b8 100644 --- a/spec/lib/gitlab/diff/position_tracer_spec.rb +++ b/spec/lib/gitlab/diff/position_tracer_spec.rb @@ -122,7 +122,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do end def delete_file(branch_name, file_name) - Files::DeleteService.new( + Files::DestroyService.new( project, current_user, start_branch: branch_name, diff --git a/spec/lib/gitlab/git/blob_snippet_spec.rb b/spec/lib/gitlab/git/blob_snippet_spec.rb index 79b1311ac91..17d6be470ac 100644 --- a/spec/lib/gitlab/git/blob_snippet_spec.rb +++ b/spec/lib/gitlab/git/blob_snippet_spec.rb @@ -11,7 +11,7 @@ describe Gitlab::Git::BlobSnippet, seed_helper: true do end context 'present lines' do - let(:snippet) { Gitlab::Git::BlobSnippet.new('master', ['wow', 'much'], 1, 'wow.rb') } + let(:snippet) { Gitlab::Git::BlobSnippet.new('master', %w(wow much), 1, 'wow.rb') } it { expect(snippet.data).to eq("wow\nmuch") } end diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb index 84f79ec2391..8049e2c120d 100644 --- a/spec/lib/gitlab/git/blob_spec.rb +++ b/spec/lib/gitlab/git/blob_spec.rb @@ -222,191 +222,6 @@ describe Gitlab::Git::Blob, seed_helper: true do end end - describe :commit do - let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } - - let(:commit_options) do - { - file: { - content: 'Lorem ipsum...', - path: 'documents/story.txt' - }, - author: { - email: 'user@example.com', - name: 'Test User', - time: Time.now - }, - committer: { - email: 'user@example.com', - name: 'Test User', - time: Time.now - }, - commit: { - message: 'Wow such commit', - branch: 'fix-mode' - } - } - end - - let(:commit_sha) { Gitlab::Git::Blob.commit(repository, commit_options) } - let(:commit) { repository.lookup(commit_sha) } - - it 'should add file with commit' do - # Commit message valid - expect(commit.message).to eq('Wow such commit') - - tree = commit.tree.to_a.find { |tree| tree[:name] == 'documents' } - - # Directory was created - expect(tree[:type]).to eq(:tree) - - # File was created - expect(repository.lookup(tree[:oid]).first[:name]).to eq('story.txt') - end - - describe "ref updating" do - it 'creates a commit but does not udate a ref' do - commit_opts = commit_options.tap{ |opts| opts[:commit][:update_ref] = false} - commit_sha = Gitlab::Git::Blob.commit(repository, commit_opts) - commit = repository.lookup(commit_sha) - - # Commit message valid - expect(commit.message).to eq('Wow such commit') - - # Does not update any related ref - expect(repository.lookup("fix-mode").oid).not_to eq(commit.oid) - expect(repository.lookup("HEAD").oid).not_to eq(commit.oid) - end - end - - describe 'reject updates' do - it 'should reject updates' do - commit_options[:file][:update] = false - commit_options[:file][:path] = 'files/executables/ls' - - expect{ commit_sha }.to raise_error('Filename already exists; update not allowed') - end - end - - describe 'file modes' do - it 'should preserve file modes with commit' do - commit_options[:file][:path] = 'files/executables/ls' - - entry = Gitlab::Git::Blob::find_entry_by_path(repository, commit.tree.oid, commit_options[:file][:path]) - expect(entry[:filemode]).to eq(0100755) - end - end - end - - describe :rename do - let(:repository) { Gitlab::Git::Repository.new(TEST_NORMAL_REPO_PATH) } - let(:rename_options) do - { - file: { - path: 'NEWCONTRIBUTING.md', - previous_path: 'CONTRIBUTING.md', - content: 'Lorem ipsum...', - update: true - }, - author: { - email: 'user@example.com', - name: 'Test User', - time: Time.now - }, - committer: { - email: 'user@example.com', - name: 'Test User', - time: Time.now - }, - commit: { - message: 'Rename readme', - branch: 'master' - } - } - end - - let(:rename_options2) do - { - file: { - content: 'Lorem ipsum...', - path: 'bin/new_executable', - previous_path: 'bin/executable', - }, - author: { - email: 'user@example.com', - name: 'Test User', - time: Time.now - }, - committer: { - email: 'user@example.com', - name: 'Test User', - time: Time.now - }, - commit: { - message: 'Updates toberenamed.txt', - branch: 'master', - update_ref: false - } - } - end - - it 'maintains file permissions when renaming' do - mode = 0o100755 - - Gitlab::Git::Blob.rename(repository, rename_options2) - - expect(repository.rugged.index.get(rename_options2[:file][:path])[:mode]).to eq(mode) - end - - it 'renames the file with commit and not change file permissions' do - ref = rename_options[:commit][:branch] - - expect(repository.rugged.index.get('CONTRIBUTING.md')).not_to be_nil - expect { Gitlab::Git::Blob.rename(repository, rename_options) }.to change { repository.commit_count(ref) }.by(1) - - expect(repository.rugged.index.get('CONTRIBUTING.md')).to be_nil - expect(repository.rugged.index.get('NEWCONTRIBUTING.md')).not_to be_nil - end - end - - describe :remove do - let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } - - let(:commit_options) do - { - file: { - path: 'README.md' - }, - author: { - email: 'user@example.com', - name: 'Test User', - time: Time.now - }, - committer: { - email: 'user@example.com', - name: 'Test User', - time: Time.now - }, - commit: { - message: 'Remove readme', - branch: 'feature' - } - } - end - - let(:commit_sha) { Gitlab::Git::Blob.remove(repository, commit_options) } - let(:commit) { repository.lookup(commit_sha) } - let(:blob) { Gitlab::Git::Blob.find(repository, commit_sha, "README.md") } - - it 'should remove file with commit' do - # Commit message valid - expect(commit.message).to eq('Remove readme') - - # File was removed - expect(blob).to be_nil - end - end - describe :lfs_pointers do context 'file a valid lfs pointer' do let(:blob) do diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb index 4fa72c565ae..47bdd7310d5 100644 --- a/spec/lib/gitlab/git/diff_collection_spec.rb +++ b/spec/lib/gitlab/git/diff_collection_spec.rb @@ -365,7 +365,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do end context 'when go over safe limits on files' do - let(:iterator) { [ fake_diff(1, 1) ] * 4 } + let(:iterator) { [fake_diff(1, 1)] * 4 } before(:each) do stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: 2, max_lines: max_lines }) diff --git a/spec/lib/gitlab/git/index_spec.rb b/spec/lib/gitlab/git/index_spec.rb new file mode 100644 index 00000000000..d0c7ca60ddc --- /dev/null +++ b/spec/lib/gitlab/git/index_spec.rb @@ -0,0 +1,220 @@ +require 'spec_helper' + +describe Gitlab::Git::Index, seed_helper: true do + let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + let(:index) { described_class.new(repository) } + + before do + index.read_tree(repository.lookup('master').tree) + end + + describe '#create' do + let(:options) do + { + content: 'Lorem ipsum...', + file_path: 'documents/story.txt' + } + end + + context 'when no file at that path exists' do + it 'creates the file in the index' do + index.create(options) + + entry = index.get(options[:file_path]) + + expect(entry).not_to be_nil + expect(repository.lookup(entry[:oid]).content).to eq(options[:content]) + end + end + + context 'when a file at that path exists' do + before do + options[:file_path] = 'files/executables/ls' + end + + it 'raises an error' do + expect { index.create(options) }.to raise_error('Filename already exists') + end + end + + context 'when content is in base64' do + before do + options[:content] = Base64.encode64(options[:content]) + options[:encoding] = 'base64' + end + + it 'decodes base64' do + index.create(options) + + entry = index.get(options[:file_path]) + expect(repository.lookup(entry[:oid]).content).to eq(Base64.decode64(options[:content])) + end + end + + context 'when content contains CRLF' do + before do + repository.autocrlf = :input + options[:content] = "Hello,\r\nWorld" + end + + it 'converts to LF' do + index.create(options) + + entry = index.get(options[:file_path]) + expect(repository.lookup(entry[:oid]).content).to eq("Hello,\nWorld") + end + end + end + + describe '#create_dir' do + let(:options) do + { + file_path: 'newdir' + } + end + + context 'when no file or dir at that path exists' do + it 'creates the dir in the index' do + index.create_dir(options) + + entry = index.get(options[:file_path] + '/.gitkeep') + + expect(entry).not_to be_nil + end + end + + context 'when a file at that path exists' do + before do + options[:file_path] = 'files/executables/ls' + end + + it 'raises an error' do + expect { index.create_dir(options) }.to raise_error('Directory already exists as a file') + end + end + + context 'when a directory at that path exists' do + before do + options[:file_path] = 'files/executables' + end + + it 'raises an error' do + expect { index.create_dir(options) }.to raise_error('Directory already exists') + end + end + end + + describe '#update' do + let(:options) do + { + content: 'Lorem ipsum...', + file_path: 'README.md' + } + end + + context 'when no file at that path exists' do + before do + options[:file_path] = 'documents/story.txt' + end + + it 'raises an error' do + expect { index.update(options) }.to raise_error("File doesn't exist") + end + end + + context 'when a file at that path exists' do + it 'updates the file in the index' do + index.update(options) + + entry = index.get(options[:file_path]) + + expect(repository.lookup(entry[:oid]).content).to eq(options[:content]) + end + + it 'preserves file mode' do + options[:file_path] = 'files/executables/ls' + + index.update(options) + + entry = index.get(options[:file_path]) + + expect(entry[:mode]).to eq(0100755) + end + end + end + + describe '#move' do + let(:options) do + { + content: 'Lorem ipsum...', + previous_path: 'README.md', + file_path: 'NEWREADME.md' + } + end + + context 'when no file at that path exists' do + it 'raises an error' do + options[:previous_path] = 'documents/story.txt' + + expect { index.move(options) }.to raise_error("File doesn't exist") + end + end + + context 'when a file at that path exists' do + it 'removes the old file in the index' do + index.move(options) + + entry = index.get(options[:previous_path]) + + expect(entry).to be_nil + end + + it 'creates the new file in the index' do + index.move(options) + + entry = index.get(options[:file_path]) + + expect(entry).not_to be_nil + expect(repository.lookup(entry[:oid]).content).to eq(options[:content]) + end + + it 'preserves file mode' do + options[:previous_path] = 'files/executables/ls' + + index.move(options) + + entry = index.get(options[:file_path]) + + expect(entry[:mode]).to eq(0100755) + end + end + end + + describe '#delete' do + let(:options) do + { + file_path: 'README.md' + } + end + + context 'when no file at that path exists' do + before do + options[:file_path] = 'documents/story.txt' + end + + it 'raises an error' do + expect { index.delete(options) }.to raise_error("File doesn't exist") + end + end + + context 'when a file at that path exists' do + it 'removes the file in the index' do + index.delete(options) + + entry = index.get(options[:file_path]) + + expect(entry).to be_nil + end + end + end +end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 2a915bf426f..3f11f0a4516 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -47,7 +47,7 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - describe :branch_names do + describe '#branch_names' do subject { repository.branch_names } it 'has SeedRepo::Repo::BRANCHES.size elements' do @@ -57,7 +57,7 @@ describe Gitlab::Git::Repository, seed_helper: true do it { is_expected.not_to include("branch-from-space") } end - describe :tag_names do + describe '#tag_names' do subject { repository.tag_names } it { is_expected.to be_kind_of Array } @@ -78,49 +78,63 @@ describe Gitlab::Git::Repository, seed_helper: true do it { expect(metadata['ArchivePath']).to end_with extenstion } end - describe :archive do + describe '#archive_prefix' do + let(:project_name) { 'project-name'} + + before do + expect(repository).to receive(:name).once.and_return(project_name) + end + + it 'returns parameterised string for a ref containing slashes' do + prefix = repository.archive_prefix('test/branch', 'SHA') + + expect(prefix).to eq("#{project_name}-test-branch-SHA") + end + end + + describe '#archive' do let(:metadata) { repository.archive_metadata('master', '/tmp') } it_should_behave_like 'archive check', '.tar.gz' end - describe :archive_zip do + describe '#archive_zip' do let(:metadata) { repository.archive_metadata('master', '/tmp', 'zip') } it_should_behave_like 'archive check', '.zip' end - describe :archive_bz2 do + describe '#archive_bz2' do let(:metadata) { repository.archive_metadata('master', '/tmp', 'tbz2') } it_should_behave_like 'archive check', '.tar.bz2' end - describe :archive_fallback do + describe '#archive_fallback' do let(:metadata) { repository.archive_metadata('master', '/tmp', 'madeup') } it_should_behave_like 'archive check', '.tar.gz' end - describe :size do + describe '#size' do subject { repository.size } it { is_expected.to be < 2 } end - describe :has_commits? do + describe '#has_commits?' do it { expect(repository.has_commits?).to be_truthy } end - describe :empty? do + describe '#empty?' do it { expect(repository.empty?).to be_falsey } end - describe :bare? do + describe '#bare?' do it { expect(repository.bare?).to be_truthy } end - describe :heads do + describe '#heads' do let(:heads) { repository.heads } subject { heads } @@ -147,7 +161,7 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - describe :ref_names do + describe '#ref_names' do let(:ref_names) { repository.ref_names } subject { ref_names } @@ -164,7 +178,7 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - describe :search_files do + describe '#search_files' do let(:results) { repository.search_files('rails', 'master') } subject { results } @@ -200,7 +214,7 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - context :submodules do + context '#submodules' do let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } context 'where repo has submodules' do @@ -264,7 +278,7 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - describe :commit_count do + describe '#commit_count' do it { expect(repository.commit_count("master")).to eq(25) } it { expect(repository.commit_count("feature")).to eq(9) } end @@ -529,7 +543,7 @@ describe Gitlab::Git::Repository, seed_helper: true do commit_with_new_name = nil rename_commit = nil - before(:all) do + before(:context) do # Add new commits so that there's a renamed file in the commit history repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged @@ -538,49 +552,119 @@ describe Gitlab::Git::Repository, seed_helper: true do commit_with_new_name = new_commit_edit_new_file(repo) end + after(:context) do + # Erase our commits so other tests get the original repo + repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged + repo.references.update("refs/heads/master", SeedRepo::LastCommit::ID) + end + context "where 'follow' == true" do - options = { ref: "master", follow: true } + let(:options) { { ref: "master", follow: true } } context "and 'path' is a directory" do - let(:log_commits) do - repository.log(options.merge(path: "encoding")) - end + it "does not follow renames" do + log_commits = repository.log(options.merge(path: "encoding")) - it "should not follow renames" do - expect(log_commits).to include(commit_with_new_name) - expect(log_commits).to include(rename_commit) - expect(log_commits).not_to include(commit_with_old_name) + aggregate_failures do + expect(log_commits).to include(commit_with_new_name) + expect(log_commits).to include(rename_commit) + expect(log_commits).not_to include(commit_with_old_name) + end end end context "and 'path' is a file that matches the new filename" do - let(:log_commits) do - repository.log(options.merge(path: "encoding/CHANGELOG")) + context 'without offset' do + it "follows renames" do + log_commits = repository.log(options.merge(path: "encoding/CHANGELOG")) + + aggregate_failures do + expect(log_commits).to include(commit_with_new_name) + expect(log_commits).to include(rename_commit) + expect(log_commits).to include(commit_with_old_name) + end + end end - it "should follow renames" do - expect(log_commits).to include(commit_with_new_name) - expect(log_commits).to include(rename_commit) - expect(log_commits).to include(commit_with_old_name) + context 'with offset=1' do + it "follows renames and skip the latest commit" do + log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1)) + + aggregate_failures do + expect(log_commits).not_to include(commit_with_new_name) + expect(log_commits).to include(rename_commit) + expect(log_commits).to include(commit_with_old_name) + end + end + end + + context 'with offset=1', 'and limit=1' do + it "follows renames, skip the latest commit and return only one commit" do + log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1, limit: 1)) + + expect(log_commits).to contain_exactly(rename_commit) + end + end + + context 'with offset=1', 'and limit=2' do + it "follows renames, skip the latest commit and return only two commits" do + log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1, limit: 2)) + + aggregate_failures do + expect(log_commits).to contain_exactly(rename_commit, commit_with_old_name) + end + end + end + + context 'with offset=2' do + it "follows renames and skip the latest commit" do + log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2)) + + aggregate_failures do + expect(log_commits).not_to include(commit_with_new_name) + expect(log_commits).not_to include(rename_commit) + expect(log_commits).to include(commit_with_old_name) + end + end + end + + context 'with offset=2', 'and limit=1' do + it "follows renames, skip the two latest commit and return only one commit" do + log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2, limit: 1)) + + expect(log_commits).to contain_exactly(commit_with_old_name) + end + end + + context 'with offset=2', 'and limit=2' do + it "follows renames, skip the two latest commit and return only one commit" do + log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2, limit: 2)) + + aggregate_failures do + expect(log_commits).not_to include(commit_with_new_name) + expect(log_commits).not_to include(rename_commit) + expect(log_commits).to include(commit_with_old_name) + end + end end end context "and 'path' is a file that matches the old filename" do - let(:log_commits) do - repository.log(options.merge(path: "CHANGELOG")) - end + it "does not follow renames" do + log_commits = repository.log(options.merge(path: "CHANGELOG")) - it "should not follow renames" do - expect(log_commits).to include(commit_with_old_name) - expect(log_commits).to include(rename_commit) - expect(log_commits).not_to include(commit_with_new_name) + aggregate_failures do + expect(log_commits).not_to include(commit_with_new_name) + expect(log_commits).to include(rename_commit) + expect(log_commits).to include(commit_with_old_name) + end end end context "unknown ref" do - let(:log_commits) { repository.log(options.merge(ref: 'unknown')) } + it "returns an empty array" do + log_commits = repository.log(options.merge(ref: 'unknown')) - it "should return empty" do expect(log_commits).to eq([]) end end @@ -699,12 +783,6 @@ describe Gitlab::Git::Repository, seed_helper: true do end end end - - after(:all) do - # Erase our commits so other tests get the original repo - repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged - repo.references.update("refs/heads/master", SeedRepo::LastCommit::ID) - end end describe "#commits_between" do @@ -844,81 +922,6 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - describe '#mkdir' do - let(:commit_options) do - { - author: { - email: 'user@example.com', - name: 'Test User', - time: Time.now - }, - committer: { - email: 'user@example.com', - name: 'Test User', - time: Time.now - }, - commit: { - message: 'Test message', - branch: 'refs/heads/fix', - } - } - end - - def generate_diff_for_path(path) - "diff --git a/#{path}/.gitkeep b/#{path}/.gitkeep -new file mode 100644 -index 0000000..e69de29 ---- /dev/null -+++ b/#{path}/.gitkeep\n" - end - - shared_examples 'mkdir diff check' do |path, expected_path| - it 'creates a directory' do - result = repository.mkdir(path, commit_options) - expect(result).not_to eq(nil) - - # Verify another mkdir doesn't create a directory that already exists - expect{ repository.mkdir(path, commit_options) }.to raise_error('Directory already exists') - end - end - - describe 'creates a directory in root directory' do - it_should_behave_like 'mkdir diff check', 'new_dir', 'new_dir' - end - - describe 'creates a directory in subdirectory' do - it_should_behave_like 'mkdir diff check', 'files/ruby/test', 'files/ruby/test' - end - - describe 'creates a directory in subdirectory with a slash' do - it_should_behave_like 'mkdir diff check', '/files/ruby/test2', 'files/ruby/test2' - end - - describe 'creates a directory in subdirectory with multiple slashes' do - it_should_behave_like 'mkdir diff check', '//files/ruby/test3', 'files/ruby/test3' - end - - describe 'handles relative paths' do - it_should_behave_like 'mkdir diff check', 'files/ruby/../test_relative', 'files/test_relative' - end - - describe 'creates nested directories' do - it_should_behave_like 'mkdir diff check', 'files/missing/test', 'files/missing/test' - end - - it 'does not attempt to create a directory with invalid relative path' do - expect{ repository.mkdir('../files/missing/test', commit_options) }.to raise_error('Invalid path') - end - - it 'does not attempt to overwrite a file' do - expect{ repository.mkdir('README.md', commit_options) }.to raise_error('Directory already exists as a file') - end - - it 'does not attempt to overwrite a directory' do - expect{ repository.mkdir('files', commit_options) }.to raise_error('Directory already exists') - end - end - describe "#ls_files" do let(:master_file_paths) { repository.ls_files("master") } let(:not_existed_branch) { repository.ls_files("not_existed_branch") } diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index a55bd4387e0..48f7754bed8 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -199,7 +199,9 @@ describe Gitlab::GitAccess, lib: true do def stub_git_hooks # Running the `pre-receive` hook is expensive, and not necessary for this test. - allow_any_instance_of(GitHooksService).to receive(:execute).and_yield + allow_any_instance_of(GitHooksService).to receive(:execute) do |service, &block| + block.call(service) + end end def merge_into_protected_branch @@ -207,13 +209,12 @@ describe Gitlab::GitAccess, lib: true do stub_git_hooks project.repository.add_branch(user, unprotected_branch, 'feature') target_branch = project.repository.lookup('feature') - source_branch = project.repository.commit_file( + source_branch = project.repository.create_file( user, FFaker::InternetSE.login_user_name, FFaker::HipsterIpsum.paragraph, message: FFaker::HipsterIpsum.sentence, - branch_name: unprotected_branch, - update: false) + branch_name: unprotected_branch) rugged = project.repository.rugged author = { email: "email@example.com", time: Time.now, name: "Example Git User" } @@ -232,11 +233,18 @@ describe Gitlab::GitAccess, lib: true do else project.team << [user, role] end + end + + permissions_matrix[role].each do |action, allowed| + context action do + subject { access.send(:check_push_access!, changes[action]) } - permissions_matrix[role].each do |action, allowed| - context action do - subject { access.send(:check_push_access!, changes[action]) } - it { expect(subject.allowed?).to allowed ? be_truthy : be_falsey } + it do + if allowed + expect { subject }.not_to raise_error + else + expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError) + end end end end @@ -301,7 +309,7 @@ describe Gitlab::GitAccess, lib: true do } } - [['feature', 'exact'], ['feat*', 'wildcard']].each do |protected_branch_name, protected_branch_type| + [%w(feature exact), ['feat*', 'wildcard']].each do |protected_branch_name, protected_branch_type| context do before { create(:protected_branch, name: protected_branch_name, project: project) } diff --git a/spec/lib/gitlab/git_spec.rb b/spec/lib/gitlab/git_spec.rb index 219198eff60..8eaf7aac264 100644 --- a/spec/lib/gitlab/git_spec.rb +++ b/spec/lib/gitlab/git_spec.rb @@ -19,7 +19,7 @@ describe Gitlab::Git, lib: true do describe 'committer_hash' do it "returns a hash containing the given email and name" do - committer_hash = Gitlab::Git::committer_hash(email: committer_email, name: committer_name) + committer_hash = Gitlab::Git.committer_hash(email: committer_email, name: committer_name) expect(committer_hash[:email]).to eq(committer_email) expect(committer_hash[:name]).to eq(committer_name) @@ -28,7 +28,7 @@ describe Gitlab::Git, lib: true do context 'when email is nil' do it "returns nil" do - committer_hash = Gitlab::Git::committer_hash(email: nil, name: committer_name) + committer_hash = Gitlab::Git.committer_hash(email: nil, name: committer_name) expect(committer_hash).to be_nil end @@ -36,7 +36,7 @@ describe Gitlab::Git, lib: true do context 'when name is nil' do it "returns nil" do - committer_hash = Gitlab::Git::committer_hash(email: committer_email, name: nil) + committer_hash = Gitlab::Git.committer_hash(email: committer_email, name: nil) expect(committer_hash).to be_nil end diff --git a/spec/lib/gitlab/gitaly_client/notifications_spec.rb b/spec/lib/gitlab/gitaly_client/notifications_spec.rb new file mode 100644 index 00000000000..a6252c99aa1 --- /dev/null +++ b/spec/lib/gitlab/gitaly_client/notifications_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe Gitlab::GitalyClient::Notifications do + let(:client) { Gitlab::GitalyClient::Notifications.new } + + before do + allow(Gitlab.config.gitaly).to receive(:socket_path).and_return('path/to/gitaly.socket') + end + + describe '#post_receive' do + let(:repo_path) { '/path/to/my_repo.git' } + + it 'sends a post_receive message' do + expect_any_instance_of(Gitaly::Notifications::Stub). + to receive(:post_receive).with(post_receive_request_with_repo_path(repo_path)) + + client.post_receive(repo_path) + end + end +end diff --git a/spec/lib/gitlab/github_import/comment_formatter_spec.rb b/spec/lib/gitlab/github_import/comment_formatter_spec.rb index e6e33d3686a..cc38872e426 100644 --- a/spec/lib/gitlab/github_import/comment_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/comment_formatter_spec.rb @@ -1,8 +1,9 @@ require 'spec_helper' describe Gitlab::GithubImport::CommentFormatter, lib: true do + let(:client) { double } let(:project) { create(:empty_project) } - let(:octocat) { double(id: 123456, login: 'octocat') } + let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') } let(:created_at) { DateTime.strptime('2013-04-10T20:09:31Z') } let(:updated_at) { DateTime.strptime('2014-03-03T18:58:10Z') } let(:base) do @@ -16,7 +17,11 @@ describe Gitlab::GithubImport::CommentFormatter, lib: true do } end - subject(:comment) { described_class.new(project, raw)} + subject(:comment) { described_class.new(project, raw, client) } + + before do + allow(client).to receive(:user).and_return(octocat) + end describe '#attributes' do context 'when do not reference a portion of the diff' do @@ -69,8 +74,15 @@ describe Gitlab::GithubImport::CommentFormatter, lib: true do context 'when author is a GitLab user' do let(:raw) { double(base.merge(user: octocat)) } - it 'returns GitLab user id as author_id' do + it 'returns GitLab user id associated with GitHub id as author_id' do gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github') + + expect(comment.attributes.fetch(:author_id)).to eq gl_user.id + end + + it 'returns GitLab user id associated with GitHub email as author_id' do + gl_user = create(:user, email: octocat.email) + expect(comment.attributes.fetch(:author_id)).to eq gl_user.id end diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb index afd78abdc9b..33d83d6d2f1 100644 --- a/spec/lib/gitlab/github_import/importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer_spec.rb @@ -44,6 +44,7 @@ describe Gitlab::GithubImport::Importer, lib: true do allow_any_instance_of(Octokit::Client).to receive(:rate_limit!).and_raise(Octokit::NotFound) allow_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_raise(Gitlab::Shell::Error) + allow_any_instance_of(Octokit::Client).to receive(:user).and_return(octocat) 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]) @@ -53,7 +54,8 @@ describe Gitlab::GithubImport::Importer, lib: true do allow_any_instance_of(Octokit::Client).to receive(:last_response).and_return(double(rels: { next: nil })) allow_any_instance_of(Octokit::Client).to receive(:releases).and_return([release1, release2]) end - let(:octocat) { double(id: 123456, login: 'octocat') } + + let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') } let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } let(:label1) do @@ -125,6 +127,7 @@ describe Gitlab::GithubImport::Importer, lib: true do ) end + let!(:user) { create(:user, email: octocat.email) } let(:repository) { double(id: 1, fork: false) } let(:source_sha) { create(:commit, project: project).id } let(:source_branch) { double(ref: 'feature', repo: repository, sha: source_sha) } diff --git a/spec/lib/gitlab/github_import/issue_formatter_spec.rb b/spec/lib/gitlab/github_import/issue_formatter_spec.rb index eec1fabab54..f34d09f2c1d 100644 --- a/spec/lib/gitlab/github_import/issue_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/issue_formatter_spec.rb @@ -1,8 +1,9 @@ require 'spec_helper' describe Gitlab::GithubImport::IssueFormatter, lib: true do + let(:client) { double } let!(:project) { create(:empty_project, namespace: create(:namespace, path: 'octocat')) } - let(:octocat) { double(id: 123456, login: 'octocat') } + let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') } let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } @@ -23,7 +24,11 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do } end - subject(:issue) { described_class.new(project, raw_data) } + subject(:issue) { described_class.new(project, raw_data, client) } + + before do + allow(client).to receive(:user).and_return(octocat) + end shared_examples 'Gitlab::GithubImport::IssueFormatter#attributes' do context 'when issue is open' do @@ -75,11 +80,17 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do expect(issue.attributes.fetch(:assignee_id)).to be_nil end - it 'returns GitLab user id as assignee_id when is a GitLab user' do + it 'returns GitLab user id associated with GitHub id as assignee_id' do gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github') expect(issue.attributes.fetch(:assignee_id)).to eq gl_user.id end + + it 'returns GitLab user id associated with GitHub email as assignee_id' do + gl_user = create(:user, email: octocat.email) + + expect(issue.attributes.fetch(:assignee_id)).to eq gl_user.id + end end context 'when it has a milestone' do @@ -100,16 +111,22 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do context 'when author is a GitLab user' do let(:raw_data) { double(base_data.merge(user: octocat)) } - it 'returns project#creator_id as author_id when is not a GitLab user' do + it 'returns project creator_id as author_id when is not a GitLab user' do expect(issue.attributes.fetch(:author_id)).to eq project.creator_id end - it 'returns GitLab user id as author_id when is a GitLab user' do + it 'returns GitLab user id associated with GitHub id as author_id' do gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github') expect(issue.attributes.fetch(:author_id)).to eq gl_user.id end + it 'returns GitLab user id associated with GitHub email as author_id' do + gl_user = create(:user, email: octocat.email) + + expect(issue.attributes.fetch(:author_id)).to eq gl_user.id + end + it 'returns description without created at tag line' do create(:omniauth_user, extern_uid: octocat.id, provider: 'github') 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 90947ff4707..e46be18aa99 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe Gitlab::GithubImport::PullRequestFormatter, lib: true do + let(:client) { double } let(:project) { create(:project, :repository) } let(:source_sha) { create(:commit, project: project).id } let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id } @@ -10,7 +11,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do let(:target_repo) { repository } let(:target_branch) { double(ref: 'master', repo: target_repo, sha: target_sha) } let(:removed_branch) { double(ref: 'removed-branch', repo: source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b') } - let(:octocat) { double(id: 123456, login: 'octocat') } + let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') } let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } let(:base_data) do @@ -32,7 +33,11 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do } end - subject(:pull_request) { described_class.new(project, raw_data) } + subject(:pull_request) { described_class.new(project, raw_data, client) } + + before do + allow(client).to receive(:user).and_return(octocat) + end shared_examples 'Gitlab::GithubImport::PullRequestFormatter#attributes' do context 'when pull request is open' do @@ -121,26 +126,38 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do expect(pull_request.attributes.fetch(:assignee_id)).to be_nil end - it 'returns GitLab user id as assignee_id when is a GitLab user' do + it 'returns GitLab user id associated with GitHub id as assignee_id' do gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github') expect(pull_request.attributes.fetch(:assignee_id)).to eq gl_user.id end + + it 'returns GitLab user id associated with GitHub email as assignee_id' do + gl_user = create(:user, email: octocat.email) + + expect(pull_request.attributes.fetch(:assignee_id)).to eq gl_user.id + end end context 'when author is a GitLab user' do let(:raw_data) { double(base_data.merge(user: octocat)) } - it 'returns project#creator_id as author_id when is not a GitLab user' do + it 'returns project creator_id as author_id when is not a GitLab user' do expect(pull_request.attributes.fetch(:author_id)).to eq project.creator_id end - it 'returns GitLab user id as author_id when is a GitLab user' do + it 'returns GitLab user id associated with GitHub id as author_id' do gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github') expect(pull_request.attributes.fetch(:author_id)).to eq gl_user.id end + it 'returns GitLab user id associated with GitHub email as author_id' do + gl_user = create(:user, email: octocat.email) + + expect(pull_request.attributes.fetch(:author_id)).to eq gl_user.id + end + it 'returns description without created at tag line' do create(:omniauth_user, extern_uid: octocat.id, provider: 'github') diff --git a/spec/lib/gitlab/github_import/user_formatter_spec.rb b/spec/lib/gitlab/github_import/user_formatter_spec.rb new file mode 100644 index 00000000000..db792233657 --- /dev/null +++ b/spec/lib/gitlab/github_import/user_formatter_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe Gitlab::GithubImport::UserFormatter, lib: true do + let(:client) { double } + let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') } + + subject(:user) { described_class.new(client, octocat) } + + before do + allow(client).to receive(:user).and_return(octocat) + end + + describe '#gitlab_id' do + context 'when GitHub user is a GitLab user' do + it 'return GitLab user id when user associated their account with GitHub' do + gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github') + + expect(user.gitlab_id).to eq gl_user.id + end + + it 'returns GitLab user id when user primary email matches GitHub email' do + gl_user = create(:user, email: octocat.email) + + expect(user.gitlab_id).to eq gl_user.id + end + + it 'returns GitLab user id when any of user linked emails matches GitHub email' do + gl_user = create(:user, email: 'johndoe@example.com') + create(:email, user: gl_user, email: octocat.email) + + expect(user.gitlab_id).to eq gl_user.id + end + end + + it 'returns nil when GitHub user is not a GitLab user' do + expect(user.gitlab_id).to be_nil + end + end +end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 06617f3b007..eef283c2460 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -153,6 +153,7 @@ project: - gitlab_issue_tracker_service - external_wiki_service - kubernetes_service +- mock_ci_service - forked_project_link - forked_from_project - forked_project_links diff --git a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb index ea65a5dfed1..e24d070706a 100644 --- a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb +++ b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb @@ -17,7 +17,7 @@ describe 'Import/Export attribute configuration', lib: true do # Remove duplicated or add missing models # - project is not part of the tree, so it has to be added manually. # - milestone, labels have both singular and plural versions in the tree, so remove the duplicates. - names.flatten.uniq - ['milestones', 'labels'] + ['project'] + names.flatten.uniq - %w(milestones labels) + ['project'] end let(:safe_attributes_file) { 'spec/lib/gitlab/import_export/safe_model_attributes.yml' } diff --git a/spec/lib/gitlab/import_export/avatar_saver_spec.rb b/spec/lib/gitlab/import_export/avatar_saver_spec.rb index d6ee94442cb..579a31ead58 100644 --- a/spec/lib/gitlab/import_export/avatar_saver_spec.rb +++ b/spec/lib/gitlab/import_export/avatar_saver_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Gitlab::ImportExport::AvatarSaver, lib: true do let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: 'test') } - let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" } + let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } let(:project_with_avatar) { create(:empty_project, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) } let(:project) { create(:empty_project) } diff --git a/spec/lib/gitlab/import_export/file_importer_spec.rb b/spec/lib/gitlab/import_export/file_importer_spec.rb index a88ddd17aca..b88b9c18c15 100644 --- a/spec/lib/gitlab/import_export/file_importer_spec.rb +++ b/spec/lib/gitlab/import_export/file_importer_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Gitlab::ImportExport::FileImporter, lib: true do let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: 'test') } - let(:export_path) { "#{Dir::tmpdir}/file_importer_spec" } + let(:export_path) { "#{Dir.tmpdir}/file_importer_spec" } let(:valid_file) { "#{shared.export_path}/valid.json" } let(:symlink_file) { "#{shared.export_path}/invalid.json" } let(:subfolder_symlink_file) { "#{shared.export_path}/subfolder/invalid.json" } diff --git a/spec/lib/gitlab/import_export/import_export_spec.rb b/spec/lib/gitlab/import_export/import_export_spec.rb index 53f7d244d88..f3fd0d82875 100644 --- a/spec/lib/gitlab/import_export/import_export_spec.rb +++ b/spec/lib/gitlab/import_export/import_export_spec.rb @@ -2,14 +2,15 @@ require 'spec_helper' describe Gitlab::ImportExport, services: true do describe 'export filename' do - let(:project) { create(:empty_project, :public, path: 'project-path') } + let(:group) { create(:group, :nested) } + let(:project) { create(:empty_project, :public, path: 'project-path', namespace: group) } it 'contains the project path' do expect(described_class.export_filename(project: project)).to include(project.path) end it 'contains the namespace path' do - expect(described_class.export_filename(project: project)).to include(project.namespace.path) + expect(described_class.export_filename(project: project)).to include(project.namespace.full_path.tr('/', '_')) end it 'does not go over a certain length' do diff --git a/spec/lib/gitlab/import_export/model_configuration_spec.rb b/spec/lib/gitlab/import_export/model_configuration_spec.rb index 9b492d1b9c7..2ede5cdd2ad 100644 --- a/spec/lib/gitlab/import_export/model_configuration_spec.rb +++ b/spec/lib/gitlab/import_export/model_configuration_spec.rb @@ -14,7 +14,7 @@ describe 'Import/Export model configuration', lib: true do # - project is not part of the tree, so it has to be added manually. # - milestone, labels have both singular and plural versions in the tree, so remove the duplicates. # - User, Author... Models we do not care about for checking models - names.flatten.uniq - ['milestones', 'labels', 'user', 'author'] + ['project'] + names.flatten.uniq - %w(milestones labels user author) + ['project'] end let(:all_models_yml) { 'spec/lib/gitlab/import_export/all_models.yml' } diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index 2e9f60432b4..c3d5c451a3c 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -2539,7 +2539,7 @@ "merge_params": { "force_remove_source_branch": null }, - "merge_when_build_succeeds": true, + "merge_when_pipeline_succeeds": true, "merge_user_id": null, "merge_commit_sha": null, "deleted_at": null, @@ -2976,7 +2976,7 @@ "merge_params": { "force_remove_source_branch": null }, - "merge_when_build_succeeds": false, + "merge_when_pipeline_succeeds": false, "merge_user_id": null, "merge_commit_sha": null, "deleted_at": null, @@ -3260,7 +3260,7 @@ "merge_params": { "force_remove_source_branch": null }, - "merge_when_build_succeeds": false, + "merge_when_pipeline_succeeds": false, "merge_user_id": null, "merge_commit_sha": null, "deleted_at": null, @@ -3544,7 +3544,7 @@ "merge_params": { "force_remove_source_branch": null }, - "merge_when_build_succeeds": false, + "merge_when_pipeline_succeeds": false, "merge_user_id": null, "merge_commit_sha": null, "deleted_at": null, @@ -4234,7 +4234,7 @@ "merge_params": { "force_remove_source_branch": null }, - "merge_when_build_succeeds": false, + "merge_when_pipeline_succeeds": false, "merge_user_id": null, "merge_commit_sha": null, "deleted_at": null, @@ -4782,7 +4782,7 @@ "merge_params": { "force_remove_source_branch": null }, - "merge_when_build_succeeds": false, + "merge_when_pipeline_succeeds": false, "merge_user_id": null, "merge_commit_sha": null, "deleted_at": null, @@ -5281,7 +5281,7 @@ "merge_params": { "force_remove_source_branch": null }, - "merge_when_build_succeeds": false, + "merge_when_pipeline_succeeds": false, "merge_user_id": null, "merge_commit_sha": null, "deleted_at": null, @@ -5541,7 +5541,7 @@ "merge_params": { "force_remove_source_branch": null }, - "merge_when_build_succeeds": false, + "merge_when_pipeline_succeeds": false, "merge_user_id": null, "merge_commit_sha": null, "deleted_at": null, @@ -6231,7 +6231,7 @@ "merge_params": { "force_remove_source_branch": null }, - "merge_when_build_succeeds": false, + "merge_when_pipeline_succeeds": false, "merge_user_id": null, "merge_commit_sha": null, "deleted_at": null, diff --git a/spec/lib/gitlab/import_export/project.light.json b/spec/lib/gitlab/import_export/project.light.json new file mode 100644 index 00000000000..a78836c3c34 --- /dev/null +++ b/spec/lib/gitlab/import_export/project.light.json @@ -0,0 +1,48 @@ +{ + "description": "Nisi et repellendus ut enim quo accusamus vel magnam.", + "visibility_level": 10, + "archived": false, + "labels": [ + { + "id": 2, + "title": "test2", + "color": "#428bca", + "project_id": 8, + "created_at": "2016-07-22T08:55:44.161Z", + "updated_at": "2016-07-22T08:55:44.161Z", + "template": false, + "description": "", + "type": "ProjectLabel", + "priorities": [ + ] + }, + { + "id": 3, + "title": "test3", + "color": "#428bca", + "group_id": 8, + "created_at": "2016-07-22T08:55:44.161Z", + "updated_at": "2016-07-22T08:55:44.161Z", + "template": false, + "description": "", + "project_id": null, + "type": "GroupLabel", + "priorities": [ + { + "id": 1, + "project_id": 5, + "label_id": 1, + "priority": 1, + "created_at": "2016-10-18T09:35:43.338Z", + "updated_at": "2016-10-18T09:35:43.338Z" + } + ] + } + ], + "snippets": [ + + ], + "hooks": [ + + ] +}
\ 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 0af13ba8e47..f4a21c24fa1 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -3,24 +3,24 @@ include ImportExport::CommonUtil describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do describe 'restore project tree' 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, :builds_disabled, :issues_disabled, name: 'project', path: 'project') } - let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) } - let(:restored_project_json) { project_tree_restorer.restore } + before(:context) do + @user = create(:user) - before do - allow(shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/') + RSpec::Mocks.with_temporary_scope do + @shared = Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') + allow(@shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/') + @project = create(:empty_project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') + project_tree_restorer = described_class.new(user: @user, shared: @shared, project: @project) + @restored_project_json = project_tree_restorer.restore + end end context 'JSON' do it 'restores models based on JSON' do - expect(restored_project_json).to be true + 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) @@ -31,62 +31,42 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do end it 'has the same label associated to two issues' do - restored_project_json - expect(ProjectLabel.find_by_title('test2').issues.count).to eq(2) end it 'has milestones associated to two separate issues' do - restored_project_json - expect(Milestone.find_by_description('test milestone').issues.count).to eq(2) end it 'creates a valid pipeline note' do - restored_project_json - expect(Ci::Pipeline.first.notes).not_to be_empty end it 'restores pipelines with missing ref' do - restored_project_json - expect(Ci::Pipeline.where(ref: nil)).not_to be_empty end it 'restores the correct event with symbolised data' do - restored_project_json - expect(Event.where.not(data: nil).first.data[:ref]).not_to be_empty end it 'preserves updated_at on issues' do - restored_project_json - issue = Issue.where(description: 'Aliquam enim illo et possimus.').first expect(issue.reload.updated_at.to_s).to eq('2016-06-14 15:02:47 UTC') end it 'contains the merge access levels on a protected branch' do - restored_project_json - expect(ProtectedBranch.first.merge_access_levels).not_to be_empty end it 'contains the push access levels on a protected branch' do - restored_project_json - expect(ProtectedBranch.first.push_access_levels).not_to be_empty end context 'event at forth level of the tree' do let(:event) { Event.where(title: 'test levels').first } - before do - restored_project_json - end - it 'restores the event' do expect(event).not_to be_nil end @@ -99,77 +79,40 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do it 'has the correct data for merge request st_diffs' do # makes sure we are renaming the custom method +utf8_st_diffs+ into +st_diffs+ - expect { restored_project_json }.to change(MergeRequestDiff.where.not(st_diffs: nil), :count).by(9) + expect(MergeRequestDiff.where.not(st_diffs: nil).count).to eq(9) end it 'has labels associated to label links, associated to issues' do - restored_project_json - expect(Label.first.label_links.first.target).not_to be_nil end it 'has project labels' do - restored_project_json - expect(ProjectLabel.count).to eq(2) end it 'has no group labels' do - restored_project_json - expect(GroupLabel.count).to eq(0) end - context 'with group' do - let!(:project) do - create(:empty_project, - :builds_disabled, - :issues_disabled, - name: 'project', - path: 'project', - group: create(:group)) - end - - it 'has group labels' do - restored_project_json - - expect(GroupLabel.count).to eq(1) - end - - it 'has label priorities' do - restored_project_json - - expect(GroupLabel.first.priorities).not_to be_empty - end - end - it 'has a project feature' do - restored_project_json - - expect(project.project_feature).not_to be_nil + expect(@project.project_feature).not_to be_nil end it 'restores the correct service' do - restored_project_json - expect(CustomIssueTrackerService.first).not_to be_nil end context 'Merge requests' do - before do - restored_project_json - end - it 'always has the new project as a target' do - expect(MergeRequest.find_by_title('MR1').target_project).to eq(project) + expect(MergeRequest.find_by_title('MR1').target_project).to eq(@project) end it 'has the same source project as originally if source/target are the same' do - expect(MergeRequest.find_by_title('MR1').source_project).to eq(project) + expect(MergeRequest.find_by_title('MR1').source_project).to eq(@project) end it 'has the new project as target if source/target differ' do - expect(MergeRequest.find_by_title('MR2').target_project).to eq(project) + expect(MergeRequest.find_by_title('MR2').target_project).to eq(@project) end it 'has no source if source/target differ' do @@ -177,39 +120,71 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do end end - context 'project.json file access check' do - it 'does not read a symlink' do - Dir.mktmpdir do |tmpdir| - setup_symlink(tmpdir, 'project.json') - allow(shared).to receive(:export_path).and_call_original - - restored_project_json + context 'tokens are regenerated' do + it 'has a new CI trigger token' do + expect(Ci::Trigger.where(token: 'cdbfasdf44a5958c83654733449e585')).to be_empty + end - expect(shared.errors.first).not_to include('test') - end + it 'has a new CI build token' do + expect(Ci::Build.where(token: 'abcd')).to be_empty end end + end + end - context 'when there is an existing build with build token' do - it 'restores project json correctly' do - create(:ci_build, token: 'abcd') + context 'Light JSON' do + let(:user) { create(:user) } + let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') } + let!(:project) { create(:empty_project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') } + let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) } + let(:restored_project_json) { project_tree_restorer.restore } - expect(restored_project_json).to be true - end - end + before do + allow(ImportExport).to receive(:project_filename).and_return('project.light.json') + allow(shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/') + end + + context 'project.json file access check' do + it 'does not read a symlink' do + Dir.mktmpdir do |tmpdir| + setup_symlink(tmpdir, 'project.json') + allow(shared).to receive(:export_path).and_call_original - context 'tokens are regenerated' do - before do restored_project_json - end - it 'has a new CI trigger token' do - expect(Ci::Trigger.where(token: 'cdbfasdf44a5958c83654733449e585')).to be_empty + expect(shared.errors.first).not_to include('test') end + end + end - it 'has a new CI build token' do - expect(Ci::Build.where(token: 'abcd')).to be_empty - end + context 'when there is an existing build with build token' do + it 'restores project json correctly' do + create(:ci_build, token: 'abcd') + + expect(restored_project_json).to be true + end + end + + context 'with group' do + let!(:project) do + create(:empty_project, + :builds_disabled, + :issues_disabled, + name: 'project', + path: 'project', + group: create(:group)) + end + + before do + restored_project_json + end + + it 'has group labels' do + expect(GroupLabel.count).to eq(1) + end + + it 'has label priorities' do + expect(GroupLabel.first.priorities).not_to be_empty end end end diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index 3628adefc0c..012c22ec5ad 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -4,7 +4,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do describe 'saves the project tree into a json object' do let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) } let(:project_tree_saver) { described_class.new(project: project, current_user: user, shared: shared) } - let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" } + let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } let(:user) { create(:user) } let(:project) { setup_project } @@ -114,7 +114,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do it 'has project and group labels' do label_types = saved_project_json['issues'].first['label_links'].map { |link| link['label']['type'] } - expect(label_types).to match_array(['ProjectLabel', 'GroupLabel']) + expect(label_types).to match_array(%w(ProjectLabel GroupLabel)) end it 'has priorities associated to labels' do diff --git a/spec/lib/gitlab/import_export/repo_bundler_spec.rb b/spec/lib/gitlab/import_export/repo_bundler_spec.rb index d39ea60ff7f..a7f4e11271e 100644 --- a/spec/lib/gitlab/import_export/repo_bundler_spec.rb +++ b/spec/lib/gitlab/import_export/repo_bundler_spec.rb @@ -4,7 +4,7 @@ describe Gitlab::ImportExport::RepoSaver, services: true do describe 'bundle a project Git repo' do let(:user) { create(:user) } let!(:project) { create(:empty_project, :public, name: 'searchable_project') } - let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" } + let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) } let(:bundler) { described_class.new(project: project, shared: shared) } diff --git a/spec/lib/gitlab/import_export/repo_restorer_spec.rb b/spec/lib/gitlab/import_export/repo_restorer_spec.rb new file mode 100644 index 00000000000..168a59e5139 --- /dev/null +++ b/spec/lib/gitlab/import_export/repo_restorer_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe Gitlab::ImportExport::RepoRestorer, services: true do + describe 'bundle a project Git repo' do + let(:user) { create(:user) } + let!(:project_with_repo) { create(:project, :test_repo, name: 'test-repo-restorer', path: 'test-repo-restorer') } + let!(:project) { create(:empty_project) } + let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } + let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) } + let(:bundler) { Gitlab::ImportExport::RepoSaver.new(project: project_with_repo, shared: shared) } + let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) } + let(:restorer) do + described_class.new(path_to_bundle: bundle_path, + shared: shared, + project: project) + end + + before do + allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) + + bundler.save + end + + after do + FileUtils.rm_rf(export_path) + FileUtils.rm_rf(project_with_repo.repository.path_to_repo) + FileUtils.rm_rf(project.repository.path_to_repo) + end + + it 'restores the repo successfully' do + expect(restorer.restore).to be true + end + + it 'has the webhooks' do + restorer.restore + + expect(Gitlab::Git::Hook.new('post-receive', project.repository.path_to_repo)).to exist + end + end +end diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index c5ac702d831..6534902b52d 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -142,7 +142,7 @@ MergeRequest: - updated_by_id - merge_error - merge_params -- merge_when_build_succeeds +- merge_when_pipeline_succeeds - merge_user_id - merge_commit_sha - deleted_at diff --git a/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb b/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb index 47d5d2fc150..071e5fac3f0 100644 --- a/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb +++ b/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb @@ -4,7 +4,7 @@ describe Gitlab::ImportExport::WikiRepoSaver, services: true do describe 'bundle a wiki Git repo' do let(:user) { create(:user) } let!(:project) { create(:empty_project, :public, name: 'searchable_project') } - let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" } + let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) } let(:wiki_bundler) { described_class.new(project: project, shared: shared) } let!(:project_wiki) { ProjectWiki.new(project, user) } diff --git a/spec/lib/gitlab/import_sources_spec.rb b/spec/lib/gitlab/import_sources_spec.rb index 8cea38e9ff8..b3b5e5e7e33 100644 --- a/spec/lib/gitlab/import_sources_spec.rb +++ b/spec/lib/gitlab/import_sources_spec.rb @@ -22,16 +22,16 @@ describe Gitlab::ImportSources do describe '.values' do it 'returns an array' do expected = - [ - 'github', - 'bitbucket', - 'gitlab', - 'google_code', - 'fogbugz', - 'git', - 'gitlab_project', - 'gitea' - ] + %w( + github + bitbucket + gitlab + google_code + fogbugz + git + gitlab_project + gitea + ) expect(described_class.values).to eq(expected) end @@ -40,15 +40,15 @@ describe Gitlab::ImportSources do describe '.importer_names' do it 'returns an array of importer names' do expected = - [ - 'github', - 'bitbucket', - 'gitlab', - 'google_code', - 'fogbugz', - 'gitlab_project', - 'gitea' - ] + %w( + github + bitbucket + gitlab + google_code + fogbugz + gitlab_project + gitea + ) expect(described_class.importer_names).to eq(expected) end diff --git a/spec/lib/gitlab/kubernetes_spec.rb b/spec/lib/gitlab/kubernetes_spec.rb index c9bd52a3b8f..91f9d06b85a 100644 --- a/spec/lib/gitlab/kubernetes_spec.rb +++ b/spec/lib/gitlab/kubernetes_spec.rb @@ -9,7 +9,7 @@ describe Gitlab::Kubernetes do let(:pod_name) { 'pod1' } let(:container_name) { 'container1' } - subject(:result) { URI::parse(container_exec_url(api_url, namespace, pod_name, container_name)) } + subject(:result) { URI.parse(container_exec_url(api_url, namespace, pod_name, container_name)) } it { expect(result.scheme).to eq('wss') } it { expect(result.host).to eq('example.com') } diff --git a/spec/lib/gitlab/ldap/auth_hash_spec.rb b/spec/lib/gitlab/ldap/auth_hash_spec.rb index 69c49051156..7a2f774b948 100644 --- a/spec/lib/gitlab/ldap/auth_hash_spec.rb +++ b/spec/lib/gitlab/ldap/auth_hash_spec.rb @@ -44,7 +44,7 @@ describe Gitlab::LDAP::AuthHash, lib: true do context "with overridden attributes" do let(:attributes) do { - 'username' => ['mail', 'email'], + 'username' => %w(mail email), 'name' => 'fullName' } end diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb index 89790c9e1af..2f3bd4393b7 100644 --- a/spec/lib/gitlab/ldap/user_spec.rb +++ b/spec/lib/gitlab/ldap/user_spec.rb @@ -95,10 +95,10 @@ describe Gitlab::LDAP::User, lib: true do it 'maintains an identity per provider' do existing_user = create(:omniauth_user, email: 'john@example.com', provider: 'twitter') - expect(existing_user.identities.count).to eql(1) + expect(existing_user.identities.count).to be(1) ldap_user.save - expect(ldap_user.gl_user.identities.count).to eql(2) + expect(ldap_user.gl_user.identities.count).to be(2) # Expect that find_by provider only returns a single instance of an identity and not an Enumerable expect(ldap_user.gl_user.identities.find_by(provider: 'twitter')).to be_instance_of Identity diff --git a/spec/lib/gitlab/metrics/instrumentation_spec.rb b/spec/lib/gitlab/metrics/instrumentation_spec.rb index d88bcae41fb..a986cb520fb 100644 --- a/spec/lib/gitlab/metrics/instrumentation_spec.rb +++ b/spec/lib/gitlab/metrics/instrumentation_spec.rb @@ -197,11 +197,13 @@ describe Gitlab::Metrics::Instrumentation do @child1 = Class.new(@dummy) do def self.child1_foo; end + def child1_bar; end end @child2 = Class.new(@child1) do def self.child2_foo; end + def child2_bar; end end end diff --git a/spec/lib/gitlab/metrics/method_call_spec.rb b/spec/lib/gitlab/metrics/method_call_spec.rb index 8d05081eecb..a247f03b2da 100644 --- a/spec/lib/gitlab/metrics/method_call_spec.rb +++ b/spec/lib/gitlab/metrics/method_call_spec.rb @@ -23,7 +23,7 @@ describe Gitlab::Metrics::MethodCall do expect(metric.values[:duration]).to be_a_kind_of(Numeric) expect(metric.values[:cpu_duration]).to be_a_kind_of(Numeric) - expect(metric.values[:call_count]).to an_instance_of(Fixnum) + expect(metric.values[:call_count]).to be_an(Integer) expect(metric.tags).to eq({ method: 'Foo#bar' }) end diff --git a/spec/lib/gitlab/metrics/metric_spec.rb b/spec/lib/gitlab/metrics/metric_spec.rb index f26fca52c50..d240b8a01fd 100644 --- a/spec/lib/gitlab/metrics/metric_spec.rb +++ b/spec/lib/gitlab/metrics/metric_spec.rb @@ -62,7 +62,7 @@ describe Gitlab::Metrics::Metric do end it 'includes the timestamp' do - expect(hash[:timestamp]).to be_an_instance_of(Fixnum) + expect(hash[:timestamp]).to be_an(Integer) end end end diff --git a/spec/lib/gitlab/metrics/system_spec.rb b/spec/lib/gitlab/metrics/system_spec.rb index 9e2ea89a712..4d94d8705fb 100644 --- a/spec/lib/gitlab/metrics/system_spec.rb +++ b/spec/lib/gitlab/metrics/system_spec.rb @@ -29,19 +29,19 @@ describe Gitlab::Metrics::System do describe '.cpu_time' do it 'returns a Fixnum' do - expect(described_class.cpu_time).to be_an_instance_of(Fixnum) + expect(described_class.cpu_time).to be_an(Integer) end end describe '.real_time' do it 'returns a Fixnum' do - expect(described_class.real_time).to be_an_instance_of(Fixnum) + expect(described_class.real_time).to be_an(Integer) end end describe '.monotonic_time' do it 'returns a Fixnum' do - expect(described_class.monotonic_time).to be_an_instance_of(Fixnum) + expect(described_class.monotonic_time).to be_an(Integer) end end end diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb index 3887c04c832..0c5a6246d85 100644 --- a/spec/lib/gitlab/metrics/transaction_spec.rb +++ b/spec/lib/gitlab/metrics/transaction_spec.rb @@ -134,7 +134,7 @@ describe Gitlab::Metrics::Transaction do series: 'rails_transactions', tags: { action: 'Foo#bar' }, values: { duration: 0.0, allocated_memory: a_kind_of(Numeric) }, - timestamp: an_instance_of(Fixnum) + timestamp: a_kind_of(Integer) } expect(Gitlab::Metrics).to receive(:submit_metrics). @@ -151,7 +151,7 @@ describe Gitlab::Metrics::Transaction do series: 'events', tags: { event: :meow }, values: { count: 1 }, - timestamp: an_instance_of(Fixnum) + timestamp: a_kind_of(Integer) } expect(Gitlab::Metrics).to receive(:submit_metrics). diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb index fc9e1cb430a..6c84a4c8b73 100644 --- a/spec/lib/gitlab/o_auth/user_spec.rb +++ b/spec/lib/gitlab/o_auth/user_spec.rb @@ -148,12 +148,14 @@ describe Gitlab::OAuth::User, lib: true do expect(gl_user).to be_valid expect(gl_user.username).to eql uid expect(gl_user.email).to eql 'johndoe@example.com' - expect(gl_user.identities.length).to eql 2 + expect(gl_user.identities.length).to be 2 identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } expect(identities_as_hash).to match_array( - [ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' }, + [ + { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' }, { provider: 'twitter', extern_uid: uid } - ]) + ] + ) end end @@ -167,12 +169,14 @@ describe Gitlab::OAuth::User, lib: true do expect(gl_user).to be_valid expect(gl_user.username).to eql 'john' expect(gl_user.email).to eql 'john@example.com' - expect(gl_user.identities.length).to eql 2 + expect(gl_user.identities.length).to be 2 identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } expect(identities_as_hash).to match_array( - [ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' }, + [ + { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' }, { provider: 'twitter', extern_uid: uid } - ]) + ] + ) end end diff --git a/spec/lib/gitlab/optimistic_locking_spec.rb b/spec/lib/gitlab/optimistic_locking_spec.rb index 498dc514c8c..acce2be93f2 100644 --- a/spec/lib/gitlab/optimistic_locking_spec.rb +++ b/spec/lib/gitlab/optimistic_locking_spec.rb @@ -1,10 +1,10 @@ require 'spec_helper' describe Gitlab::OptimisticLocking, lib: true do - describe '#retry_lock' do - let!(:pipeline) { create(:ci_pipeline) } - let!(:pipeline2) { Ci::Pipeline.find(pipeline.id) } + let!(:pipeline) { create(:ci_pipeline) } + let!(:pipeline2) { Ci::Pipeline.find(pipeline.id) } + describe '#retry_lock' do it 'does not reload object if state changes' do expect(pipeline).not_to receive(:reload) expect(pipeline).to receive(:succeed).and_call_original @@ -36,4 +36,17 @@ describe Gitlab::OptimisticLocking, lib: true do end.to raise_error(ActiveRecord::StaleObjectError) end end + + describe '#retry_optimistic_lock' do + context 'when locking module is mixed in' do + let(:unlockable) do + Class.new.include(described_class).new + end + + it 'is an alias for retry_lock' do + expect(unlockable.method(:retry_optimistic_lock)) + .to eq unlockable.method(:retry_lock) + end + end + end end diff --git a/spec/lib/gitlab/other_markup.rb b/spec/lib/gitlab/other_markup.rb new file mode 100644 index 00000000000..8f5a353b381 --- /dev/null +++ b/spec/lib/gitlab/other_markup.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe Gitlab::OtherMarkup, lib: true do + context "XSS Checks" do + links = { + 'links' => { + file: 'file.rdoc', + input: 'XSS[JaVaScriPt:alert(1)]', + output: '<p><a>XSS</a></p>' + } + } + links.each do |name, data| + it "does not convert dangerous #{name} into HTML" do + expect(render(data[:file], data[:input], context)).to eql data[:output] + end + end + end + + def render(*args) + described_class.render(*args) + end +end diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb index 6b689c41ef6..84cfd934fa0 100644 --- a/spec/lib/gitlab/reference_extractor_spec.rb +++ b/spec/lib/gitlab/reference_extractor_spec.rb @@ -42,14 +42,85 @@ describe Gitlab::ReferenceExtractor, lib: true do > @offteam }) + expect(subject.users).to match_array([]) end + describe 'directly addressed users' do + before do + @u_foo = create(:user, username: 'foo') + @u_foo2 = create(:user, username: 'foo2') + @u_foo3 = create(:user, username: 'foo3') + @u_foo4 = create(:user, username: 'foo4') + @u_foo5 = create(:user, username: 'foo5') + + @u_bar = create(:user, username: 'bar') + @u_bar2 = create(:user, username: 'bar2') + @u_bar3 = create(:user, username: 'bar3') + @u_bar4 = create(:user, username: 'bar4') + + @u_tom = create(:user, username: 'tom') + @u_tom2 = create(:user, username: 'tom2') + end + + context 'when a user is directly addressed' do + it 'accesses the user object which is mentioned in the beginning of the line' do + subject.analyze('@foo What do you think? cc: @bar, @tom') + + expect(subject.directly_addressed_users).to match_array([@u_foo]) + end + + it "doesn't access the user object if it's not mentioned in the beginning of the line" do + subject.analyze('What do you think? cc: @bar') + + expect(subject.directly_addressed_users).to be_empty + end + end + + context 'when multiple users are addressed' do + it 'accesses the user objects which are mentioned in the beginning of the line' do + subject.analyze('@foo @bar What do you think? cc: @tom') + + expect(subject.directly_addressed_users).to match_array([@u_foo, @u_bar]) + end + + it "doesn't access the user objects if they are not mentioned in the beginning of the line" do + subject.analyze('What do you think? cc: @foo @bar @tom') + + expect(subject.directly_addressed_users).to be_empty + end + end + + context 'when multiple users are addressed in different paragraphs' do + it 'accesses user objects which are mentioned in the beginning of each paragraph' do + subject.analyze <<-NOTE.strip_heredoc + @foo What do you think? cc: @tom + + - @bar can you please have a look? + + >>> + @foo2 what do you think? cc: @bar2 + >>> + + @foo3 @foo4 thank you! + + > @foo5 well done! + + 1. @bar3 Can you please check? cc: @tom2 + 2. @bar4 What do you this of this MR? + NOTE + + expect(subject.directly_addressed_users).to match_array([@u_foo, @u_foo3, @u_foo4]) + end + end + end + it 'accesses valid issue objects' do @i0 = create(:issue, project: project) @i1 = create(:issue, project: project) subject.analyze("#{@i0.to_reference}, #{@i1.to_reference}, and #{Issue.reference_prefix}999.") + expect(subject.issues).to match_array([@i0, @i1]) end @@ -58,6 +129,7 @@ describe Gitlab::ReferenceExtractor, lib: true do @m1 = create(:merge_request, source_project: project, target_project: project, source_branch: 'feature_conflict') subject.analyze("!999, !#{@m1.iid}, and !#{@m0.iid}.") + expect(subject.merge_requests).to match_array([@m1, @m0]) end @@ -67,6 +139,7 @@ describe Gitlab::ReferenceExtractor, lib: true do @l2 = create(:label) subject.analyze("~#{@l0.id}, ~999, ~#{@l2.id}, ~#{@l1.id}") + expect(subject.labels).to match_array([@l0, @l1]) end @@ -76,6 +149,7 @@ describe Gitlab::ReferenceExtractor, lib: true do @s2 = create(:project_snippet) subject.analyze("$#{@s0.id}, $999, $#{@s2.id}, $#{@s1.id}") + expect(subject.snippets).to match_array([@s0, @s1]) end @@ -127,6 +201,7 @@ describe Gitlab::ReferenceExtractor, lib: true do it 'handles project issue references' do subject.analyze("this refers issue #{issue.to_reference(project)}") + extracted = subject.issues expect(extracted.size).to eq(1) expect(extracted).to match_array([issue]) diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index c78cd30157e..ba45e2d758c 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -2,47 +2,64 @@ require 'spec_helper' describe Gitlab::Regex, lib: true do - describe 'project path regex' do - it { expect('gitlab-ce').to match(Gitlab::Regex.project_path_regex) } - it { expect('gitlab_git').to match(Gitlab::Regex.project_path_regex) } - it { expect('_underscore.js').to match(Gitlab::Regex.project_path_regex) } - it { expect('100px.com').to match(Gitlab::Regex.project_path_regex) } - it { expect('?gitlab').not_to match(Gitlab::Regex.project_path_regex) } - it { expect('git lab').not_to match(Gitlab::Regex.project_path_regex) } - it { expect('gitlab.git').not_to match(Gitlab::Regex.project_path_regex) } + describe '.project_path_regex' do + subject { described_class.project_path_regex } + + it { is_expected.to match('gitlab-ce') } + it { is_expected.to match('gitlab_git') } + it { is_expected.to match('_underscore.js') } + it { is_expected.to match('100px.com') } + it { is_expected.not_to match('?gitlab') } + it { is_expected.not_to match('git lab') } + it { is_expected.not_to match('gitlab.git') } end - describe 'project name regex' do - it { expect('gitlab-ce').to match(Gitlab::Regex.project_name_regex) } - it { expect('GitLab CE').to match(Gitlab::Regex.project_name_regex) } - it { expect('100 lines').to match(Gitlab::Regex.project_name_regex) } - it { expect('gitlab.git').to match(Gitlab::Regex.project_name_regex) } - it { expect('Český název').to match(Gitlab::Regex.project_name_regex) } - it { expect('Dash – is this').to match(Gitlab::Regex.project_name_regex) } - it { expect('?gitlab').not_to match(Gitlab::Regex.project_name_regex) } + describe '.project_name_regex' do + subject { described_class.project_name_regex } + + it { is_expected.to match('gitlab-ce') } + it { is_expected.to match('GitLab CE') } + it { is_expected.to match('100 lines') } + it { is_expected.to match('gitlab.git') } + it { is_expected.to match('Český název') } + it { is_expected.to match('Dash – is this') } + it { is_expected.not_to match('?gitlab') } end - describe 'file name regex' do - it { expect('foo@bar').to match(Gitlab::Regex.file_name_regex) } + describe '.file_name_regex' do + subject { described_class.file_name_regex } + + it { is_expected.to match('foo@bar') } end - describe 'file path regex' do - it { expect('foo@/bar').to match(Gitlab::Regex.file_path_regex) } + describe '.file_path_regex' do + subject { described_class.file_path_regex } + + it { is_expected.to match('foo@/bar') } end - describe 'environment slug regex' do - def be_matched - match(Gitlab::Regex.environment_slug_regex) - end + describe '.environment_slug_regex' do + subject { described_class.environment_slug_regex } + + it { is_expected.to match('foo') } + it { is_expected.to match('foo-1') } + it { is_expected.not_to match('FOO') } + it { is_expected.not_to match('foo/1') } + it { is_expected.not_to match('foo.1') } + it { is_expected.not_to match('foo*1') } + it { is_expected.not_to match('9foo') } + it { is_expected.not_to match('foo-') } + end - it { expect('foo').to be_matched } - it { expect('foo-1').to be_matched } + describe 'FULL_NAMESPACE_REGEX_STR' do + subject { %r{\A#{Gitlab::Regex::FULL_NAMESPACE_REGEX_STR}\z} } - it { expect('FOO').not_to be_matched } - it { expect('foo/1').not_to be_matched } - it { expect('foo.1').not_to be_matched } - it { expect('foo*1').not_to be_matched } - it { expect('9foo').not_to be_matched } - it { expect('foo-').not_to be_matched } + it { is_expected.to match('gitlab.org') } + it { is_expected.to match('gitlab.org/gitlab-git') } + it { is_expected.not_to match('gitlab.org.') } + it { is_expected.not_to match('gitlab.org/') } + it { is_expected.not_to match('/gitlab.org') } + it { is_expected.not_to match('gitlab.git') } + it { is_expected.not_to match('gitlab git') } end end diff --git a/spec/lib/gitlab/saml/user_spec.rb b/spec/lib/gitlab/saml/user_spec.rb index 02c139f1a0d..4f6ef3c10fc 100644 --- a/spec/lib/gitlab/saml/user_spec.rb +++ b/spec/lib/gitlab/saml/user_spec.rb @@ -155,11 +155,10 @@ describe Gitlab::Saml::User, lib: true do expect(gl_user).to be_valid expect(gl_user.username).to eql uid expect(gl_user.email).to eql 'john@mail.com' - expect(gl_user.identities.length).to eql 2 + expect(gl_user.identities.length).to be 2 identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } - expect(identities_as_hash).to match_array([ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' }, - { provider: 'saml', extern_uid: uid } - ]) + expect(identities_as_hash).to match_array([{ provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' }, + { provider: 'saml', extern_uid: uid }]) end end @@ -178,11 +177,10 @@ describe Gitlab::Saml::User, lib: true do expect(gl_user).to be_valid expect(gl_user.username).to eql 'john' expect(gl_user.email).to eql 'john@mail.com' - expect(gl_user.identities.length).to eql 2 + expect(gl_user.identities.length).to be 2 identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } - expect(identities_as_hash).to match_array([ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' }, - { provider: 'saml', extern_uid: uid } - ]) + expect(identities_as_hash).to match_array([{ provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' }, + { provider: 'saml', extern_uid: uid }]) end it 'saves successfully on subsequent tries, when both identities are present' do @@ -204,11 +202,10 @@ describe Gitlab::Saml::User, lib: true do local_gl_user = local_saml_user.gl_user expect(local_gl_user).to be_valid - expect(local_gl_user.identities.length).to eql 2 + expect(local_gl_user.identities.length).to be 2 identities_as_hash = local_gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } - expect(identities_as_hash).to match_array([ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' }, - { provider: 'saml', extern_uid: 'uid=user1,ou=People,dc=example' } - ]) + expect(identities_as_hash).to match_array([{ provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' }, + { provider: 'saml', extern_uid: 'uid=user1,ou=People,dc=example' }]) end end end diff --git a/spec/lib/gitlab/serializer/ci/variables_spec.rb b/spec/lib/gitlab/serializer/ci/variables_spec.rb index b810c68ea03..c4b7fda5dbb 100644 --- a/spec/lib/gitlab/serializer/ci/variables_spec.rb +++ b/spec/lib/gitlab/serializer/ci/variables_spec.rb @@ -13,6 +13,7 @@ describe Gitlab::Serializer::Ci::Variables do it 'converts keys into strings' do is_expected.to eq([ { key: 'key', value: 'value', public: true }, - { key: 'wee', value: 1, public: false }]) + { key: 'wee', value: 1, public: false } + ]) end end diff --git a/spec/lib/gitlab/sidekiq_status_spec.rb b/spec/lib/gitlab/sidekiq_status_spec.rb index 0aa36a3416b..56f06b61afb 100644 --- a/spec/lib/gitlab/sidekiq_status_spec.rb +++ b/spec/lib/gitlab/sidekiq_status_spec.rb @@ -39,6 +39,32 @@ describe Gitlab::SidekiqStatus do end end + describe '.num_running', :redis do + it 'returns 0 if all jobs have been completed' do + expect(described_class.num_running(%w(123))).to eq(0) + end + + it 'returns 2 if two jobs are still running' do + described_class.set('123') + described_class.set('456') + + expect(described_class.num_running(%w(123 456 789))).to eq(2) + end + end + + describe '.num_completed', :redis do + it 'returns 1 if all jobs have been completed' do + expect(described_class.num_completed(%w(123))).to eq(1) + end + + it 'returns 1 if a job has not yet been completed' do + described_class.set('123') + described_class.set('456') + + expect(described_class.num_completed(%w(123 456 789))).to eq(1) + end + end + describe '.key_for' do it 'returns the key for a job ID' do key = described_class.key_for('123') diff --git a/spec/lib/gitlab/slash_commands/extractor_spec.rb b/spec/lib/gitlab/slash_commands/extractor_spec.rb index 1e4954c4af8..d7f77486b3e 100644 --- a/spec/lib/gitlab/slash_commands/extractor_spec.rb +++ b/spec/lib/gitlab/slash_commands/extractor_spec.rb @@ -81,6 +81,14 @@ describe Gitlab::SlashCommands::Extractor do let(:original_msg) { "/assign @joe\nworld" } let(:final_msg) { "world" } end + + it 'allows slash in command arguments' do + msg = "/assign @joe / @jane\nworld" + msg, commands = extractor.extract_commands(msg) + + expect(commands).to eq [['assign', '@joe / @jane']] + expect(msg).to eq 'world' + end end context 'in the middle of content' do diff --git a/spec/lib/gitlab/template/issue_template_spec.rb b/spec/lib/gitlab/template/issue_template_spec.rb index 1335a2b8f35..9213ced7b19 100644 --- a/spec/lib/gitlab/template/issue_template_spec.rb +++ b/spec/lib/gitlab/template/issue_template_spec.rb @@ -11,7 +11,8 @@ describe Gitlab::Template::IssueTemplate do create_template: { user: user, access: Gitlab::Access::MASTER, - path: 'issue_templates' }) + path: 'issue_templates' + }) end describe '.all' do diff --git a/spec/lib/gitlab/template/merge_request_template_spec.rb b/spec/lib/gitlab/template/merge_request_template_spec.rb index 320b870309a..77dd3079e22 100644 --- a/spec/lib/gitlab/template/merge_request_template_spec.rb +++ b/spec/lib/gitlab/template/merge_request_template_spec.rb @@ -11,7 +11,8 @@ describe Gitlab::Template::MergeRequestTemplate do create_template: { user: user, access: Gitlab::Access::MASTER, - path: 'merge_request_templates' }) + path: 'merge_request_templates' + }) end describe '.all' do diff --git a/spec/lib/gitlab/themes_spec.rb b/spec/lib/gitlab/themes_spec.rb deleted file mode 100644 index 7a140518dd2..00000000000 --- a/spec/lib/gitlab/themes_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Themes, lib: true do - describe '.body_classes' do - it 'returns a space-separated list of class names' do - css = described_class.body_classes - - expect(css).to include('ui_graphite') - expect(css).to include(' ui_charcoal ') - expect(css).to include(' ui_blue') - end - end - - describe '.by_id' do - it 'returns a Theme by its ID' do - expect(described_class.by_id(1).name).to eq 'Graphite' - expect(described_class.by_id(6).name).to eq 'Blue' - end - end - - describe '.default' do - it 'returns the default application theme' do - allow(described_class).to receive(:default_id).and_return(2) - expect(described_class.default.id).to eq 2 - end - - it 'prevents an infinite loop when configuration default is invalid' do - default = described_class::APPLICATION_DEFAULT - themes = described_class::THEMES - - config = double(default_theme: 0).as_null_object - allow(Gitlab).to receive(:config).and_return(config) - expect(described_class.default.id).to eq default - - config = double(default_theme: themes.size + 5).as_null_object - allow(Gitlab).to receive(:config).and_return(config) - expect(described_class.default.id).to eq default - end - end - - describe '.each' do - it 'passes the block to the THEMES Array' do - ids = [] - described_class.each { |theme| ids << theme.id } - expect(ids).not_to be_empty - end - end -end diff --git a/spec/lib/gitlab/upgrader_spec.rb b/spec/lib/gitlab/upgrader_spec.rb index edadab043d7..fcfd8d58b70 100644 --- a/spec/lib/gitlab/upgrader_spec.rb +++ b/spec/lib/gitlab/upgrader_spec.rb @@ -32,7 +32,8 @@ describe Gitlab::Upgrader, lib: true do '43af3e65a486a9237f29f56d96c3b3da59c24ae0 refs/tags/v7.11.2', 'dac18e7728013a77410e926a1e64225703754a2d refs/tags/v7.11.2^{}', '0bf21fd4b46c980c26fd8c90a14b86a4d90cc950 refs/tags/v7.9.4', - 'b10de29edbaff7219547dc506cb1468ee35065c3 refs/tags/v7.9.4^{}']) + 'b10de29edbaff7219547dc506cb1468ee35065c3 refs/tags/v7.9.4^{}' + ]) expect(upgrader.latest_version_raw).to eq("v7.11.2") end end diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb index d5d87310874..56772409989 100644 --- a/spec/lib/gitlab/utils_spec.rb +++ b/spec/lib/gitlab/utils_spec.rb @@ -1,7 +1,5 @@ describe Gitlab::Utils, lib: true do - def to_boolean(value) - described_class.to_boolean(value) - end + delegate :to_boolean, to: :described_class describe '.to_boolean' do it 'accepts booleans' do diff --git a/spec/lib/mattermost/command_spec.rb b/spec/lib/mattermost/command_spec.rb index 5ccf1100898..4b5938edeb9 100644 --- a/spec/lib/mattermost/command_spec.rb +++ b/spec/lib/mattermost/command_spec.rb @@ -13,8 +13,7 @@ describe Mattermost::Command do describe '#create' do let(:params) do { team_id: 'abc', - trigger: 'gitlab' - } + trigger: 'gitlab' } end subject { described_class.new(nil).create(params) } @@ -24,7 +23,8 @@ describe Mattermost::Command do stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create'). with(body: { team_id: 'abc', - trigger: 'gitlab' }.to_json). + trigger: 'gitlab' + }.to_json). to_return( status: 200, headers: { 'Content-Type' => 'application/json' }, diff --git a/spec/lib/mattermost/team_spec.rb b/spec/lib/mattermost/team_spec.rb index 2d14be6bcc2..6a9c7cebfff 100644 --- a/spec/lib/mattermost/team_spec.rb +++ b/spec/lib/mattermost/team_spec.rb @@ -14,18 +14,19 @@ describe Mattermost::Team do context 'for valid request' do let(:response) do [{ - "id" => "xiyro8huptfhdndadpz8r3wnbo", - "create_at" => 1482174222155, - "update_at" => 1482174222155, - "delete_at" => 0, - "display_name" => "chatops", - "name" => "chatops", - "email" => "admin@example.com", - "type" => "O", - "company_name" => "", - "allowed_domains" => "", - "invite_id" => "o4utakb9jtb7imctdfzbf9r5ro", - "allow_open_invite" => false }] + "id" => "xiyro8huptfhdndadpz8r3wnbo", + "create_at" => 1482174222155, + "update_at" => 1482174222155, + "delete_at" => 0, + "display_name" => "chatops", + "name" => "chatops", + "email" => "admin@example.com", + "type" => "O", + "company_name" => "", + "allowed_domains" => "", + "invite_id" => "o4utakb9jtb7imctdfzbf9r5ro", + "allow_open_invite" => false + }] end before do diff --git a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb index 6a93deb5412..b6d678bac18 100644 --- a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb +++ b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb @@ -62,7 +62,7 @@ describe MigrateProcessCommitWorkerJobs do end def pop_job - JSON.load(Sidekiq.redis { |r| r.lpop('queue:process_commit') }) + JSON.parse(Sidekiq.redis { |r| r.lpop('queue:process_commit') }) end before do @@ -198,7 +198,7 @@ describe MigrateProcessCommitWorkerJobs do let(:job) do migration.down - JSON.load(Sidekiq.redis { |r| r.lpop('queue:process_commit') }) + JSON.parse(Sidekiq.redis { |r| r.lpop('queue:process_commit') }) end it 'includes the project ID' do diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb index c4486a32082..4e71597521d 100644 --- a/spec/models/abuse_report_spec.rb +++ b/spec/models/abuse_report_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' RSpec.describe AbuseReport, type: :model do subject { create(:abuse_report) } - let(:user) { create(:user) } + let(:user) { create(:admin) } it { expect(subject).to be_valid } diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index b950fcdd81a..01ca1584ed2 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -29,6 +29,40 @@ describe ApplicationSetting, models: true do it { is_expected.not_to allow_value(['test']).for(:disabled_oauth_sign_in_sources) } end + describe 'default_artifacts_expire_in' do + it 'sets an error if it cannot parse' do + setting.update(default_artifacts_expire_in: 'a') + + expect_invalid + end + + it 'sets an error if it is blank' do + setting.update(default_artifacts_expire_in: ' ') + + expect_invalid + end + + it 'sets the value if it is valid' do + setting.update(default_artifacts_expire_in: '30 days') + + expect(setting).to be_valid + expect(setting.default_artifacts_expire_in).to eq('30 days') + end + + it 'sets the value if it is 0' do + setting.update(default_artifacts_expire_in: '0') + + expect(setting).to be_valid + expect(setting.default_artifacts_expire_in).to eq('0') + end + + def expect_invalid + expect(setting).to be_invalid + expect(setting.errors.messages) + .to have_key(:default_artifacts_expire_in) + end + end + it { is_expected.to validate_presence_of(:max_attachment_size) } it do @@ -62,9 +96,9 @@ describe ApplicationSetting, models: true do describe 'inclusion' do it { is_expected.to allow_value('custom1').for(:repository_storages) } - it { is_expected.to allow_value(['custom2', 'custom3']).for(:repository_storages) } + it { is_expected.to allow_value(%w(custom2 custom3)).for(:repository_storages) } it { is_expected.not_to allow_value('alternative').for(:repository_storages) } - it { is_expected.not_to allow_value(['alternative', 'custom1']).for(:repository_storages) } + it { is_expected.not_to allow_value(%w(alternative custom1)).for(:repository_storages) } end describe 'presence' do @@ -83,7 +117,7 @@ describe ApplicationSetting, models: true do describe '#repository_storage' do it 'returns the first storage' do - setting.repository_storages = ['good', 'bad'] + setting.repository_storages = %w(good bad) expect(setting.repository_storage).to eq('good') end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 4080092405d..5743c555cbe 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe Ci::Build, :models do + let(:user) { create(:user) } let(:project) { create(:project, :repository) } let(:build) { create(:ci_build, pipeline: pipeline) } let(:test_trace) { 'This is a test' } @@ -161,11 +162,17 @@ describe Ci::Build, :models do is_expected.to be_nil end - it 'when resseting value' do + it 'when resetting value' do build.artifacts_expire_in = nil is_expected.to be_nil end + + it 'when setting to 0' do + build.artifacts_expire_in = '0' + + is_expected.to be_nil + end end describe '#commit' do @@ -174,20 +181,6 @@ describe Ci::Build, :models do end end - describe '#create_from' do - before do - build.status = 'success' - build.save - end - let(:create_from_build) { Ci::Build.create_from build } - - it 'exists a pending task' do - expect(Ci::Build.pending.count(:all)).to eq 0 - create_from_build - expect(Ci::Build.pending.count(:all)).to be > 0 - end - end - describe '#depends_on_builds' do let!(:build) { create(:ci_build, pipeline: pipeline, name: 'build', stage_idx: 0, stage: 'build') } let!(:rspec_test) { create(:ci_build, pipeline: pipeline, name: 'rspec', stage_idx: 1, stage: 'test') } @@ -207,14 +200,16 @@ describe Ci::Build, :models do end it 'expects to have retried builds instead the original ones' do - retried_rspec = Ci::Build.retry(rspec_test) - expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, retried_rspec.id, rubocop_test.id) + project.add_developer(user) + + retried_rspec = Ci::Build.retry(rspec_test, user) + + expect(staging.depends_on_builds.map(&:id)) + .to contain_exactly(build.id, retried_rspec.id, rubocop_test.id) end end describe '#detailed_status' do - let(:user) { create(:user) } - it 'returns a detailed status' do expect(build.detailed_status(user)) .to be_a Gitlab::Ci::Status::Build::Cancelable @@ -813,12 +808,16 @@ describe Ci::Build, :models do subject { build.other_actions } + before do + project.add_developer(user) + end + it 'returns other actions' do is_expected.to contain_exactly(other_build) end context 'when build is retried' do - let!(:new_build) { Ci::Build.retry(build) } + let!(:new_build) { Ci::Build.retry(build, user) } it 'does not return any of them' do is_expected.not_to include(build, new_build) @@ -826,7 +825,7 @@ describe Ci::Build, :models do end context 'when other build is retried' do - let!(:retried_build) { Ci::Build.retry(other_build) } + let!(:retried_build) { Ci::Build.retry(other_build, user) } it 'returns a retried build' do is_expected.to contain_exactly(retried_build) @@ -857,21 +856,29 @@ describe Ci::Build, :models do describe '#play' do let(:build) { create(:ci_build, :manual, pipeline: pipeline) } - subject { build.play } + before do + project.add_developer(user) + end - it 'enqueues a build' do - is_expected.to be_pending - is_expected.to eq(build) + context 'when build is manual' do + it 'enqueues a build' do + new_build = build.play(user) + + expect(new_build).to be_pending + expect(new_build).to eq(build) + end end - context 'for successful build' do + context 'when build is passed' do before do build.update(status: 'success') end it 'creates a new build' do - is_expected.to be_pending - is_expected.not_to eq(build) + new_build = build.play(user) + + expect(new_build).to be_pending + expect(new_build).not_to eq(build) end end end @@ -1224,8 +1231,8 @@ describe Ci::Build, :models do { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true }, { key: 'CI_PROJECT_ID', value: project.id.to_s, public: true }, { key: 'CI_PROJECT_NAME', value: project.path, public: true }, - { key: 'CI_PROJECT_PATH', value: project.path_with_namespace, public: true }, - { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.path, public: true }, + { key: 'CI_PROJECT_PATH', value: project.full_path, public: true }, + { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path, public: true }, { key: 'CI_PROJECT_URL', value: project.web_url, public: true }, { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true } ] @@ -1246,12 +1253,9 @@ describe Ci::Build, :models do end context 'when build has user' do - let(:user) { create(:user, username: 'starter') } let(:user_variables) do - [ - { key: 'GITLAB_USER_ID', value: user.id.to_s, public: true }, - { key: 'GITLAB_USER_EMAIL', value: user.email, public: true } - ] + [{ key: 'GITLAB_USER_ID', value: user.id.to_s, public: true }, + { key: 'GITLAB_USER_EMAIL', value: user.email, public: true }] end before do @@ -1408,7 +1412,7 @@ describe Ci::Build, :models do end context 'when runner is assigned to build' do - let(:runner) { create(:ci_runner, description: 'description', tag_list: ['docker', 'linux']) } + let(:runner) { create(:ci_runner, description: 'description', tag_list: %w(docker linux)) } before do build.update(runner: runner) diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 426be74cd02..c2fc8c02bb3 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -3,8 +3,12 @@ require 'spec_helper' describe Ci::Pipeline, models: true do include EmailHelpers - let(:project) { FactoryGirl.create :empty_project } - let(:pipeline) { FactoryGirl.create :ci_empty_pipeline, status: 'created', project: project } + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + + let(:pipeline) do + create(:ci_empty_pipeline, status: :created, project: project) + end it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:user) } @@ -164,9 +168,9 @@ describe Ci::Pipeline, models: true do end it 'returns list of stages with correct statuses' do - expect(statuses).to eq([['build', 'failed'], - ['test', 'success'], - ['deploy', 'running']]) + expect(statuses).to eq([%w(build failed), + %w(test success), + %w(deploy running)]) end context 'when commit status is retried' do @@ -179,9 +183,9 @@ describe Ci::Pipeline, models: true do end it 'ignores the previous state' do - expect(statuses).to eq([['build', 'success'], - ['test', 'success'], - ['deploy', 'running']]) + expect(statuses).to eq([%w(build success), + %w(test success), + %w(deploy running)]) end end end @@ -195,7 +199,7 @@ describe Ci::Pipeline, models: true do describe '#stages_name' do it 'returns a valid names of stages' do - expect(pipeline.stages_name).to eq(['build', 'test', 'deploy']) + expect(pipeline.stages_name).to eq(%w(build test deploy)) end end end @@ -503,7 +507,9 @@ describe Ci::Pipeline, models: true do end describe '#status' do - let!(:build) { create(:ci_build, :created, pipeline: pipeline, name: 'test') } + let(:build) do + create(:ci_build, :created, pipeline: pipeline, name: 'test') + end subject { pipeline.reload.status } @@ -545,13 +551,21 @@ describe Ci::Pipeline, models: true do build.cancel end - it { is_expected.to eq('canceled') } + context 'when build is pending' do + let(:build) do + create(:ci_build, :pending, pipeline: pipeline) + end + + it { is_expected.to eq('canceled') } + end end context 'on failure and build retry' do before do build.drop - Ci::Build.retry(build) + project.add_developer(user) + + Ci::Build.retry(build, user) end # We are changing a state: created > failed > running @@ -563,8 +577,6 @@ describe Ci::Pipeline, models: true do end describe '#detailed_status' do - let(:user) { create(:user) } - subject { pipeline.detailed_status(user) } context 'when pipeline is created' do @@ -720,7 +732,7 @@ describe Ci::Pipeline, models: true do describe '#cancel_running' do let(:latest_status) { pipeline.statuses.pluck(:status) } - context 'when there is a running external job and created build' do + context 'when there is a running external job and a regular job' do before do create(:ci_build, :running, pipeline: pipeline) create(:generic_commit_status, :running, pipeline: pipeline) @@ -733,7 +745,7 @@ describe Ci::Pipeline, models: true do end end - context 'when builds are in different stages' do + context 'when jobs are in different stages' do before do create(:ci_build, :running, stage_idx: 0, pipeline: pipeline) create(:ci_build, :running, stage_idx: 1, pipeline: pipeline) @@ -745,17 +757,34 @@ describe Ci::Pipeline, models: true do expect(latest_status).to contain_exactly('canceled', 'canceled') end end + + context 'when there are created builds present in the pipeline' do + before do + create(:ci_build, :running, stage_idx: 0, pipeline: pipeline) + create(:ci_build, :created, stage_idx: 1, pipeline: pipeline) + + pipeline.cancel_running + end + + it 'cancels created builds' do + expect(latest_status).to eq %w(canceled canceled) + end + end end describe '#retry_failed' do let(:latest_status) { pipeline.statuses.latest.pluck(:status) } + before do + project.add_developer(user) + end + context 'when there is a failed build and failed external status' do before do create(:ci_build, :failed, name: 'build', pipeline: pipeline) create(:generic_commit_status, :failed, name: 'jenkins', pipeline: pipeline) - pipeline.retry_failed(create(:user)) + pipeline.retry_failed(user) end it 'retries only build' do @@ -768,11 +797,11 @@ describe Ci::Pipeline, models: true do create(:ci_build, :failed, name: 'build', stage_idx: 0, pipeline: pipeline) create(:ci_build, :failed, name: 'jenkins', stage_idx: 1, pipeline: pipeline) - pipeline.retry_failed(create(:user)) + pipeline.retry_failed(user) end it 'retries both builds' do - expect(latest_status).to contain_exactly('pending', 'pending') + expect(latest_status).to contain_exactly('pending', 'created') end end @@ -781,11 +810,11 @@ describe Ci::Pipeline, models: true do create(:ci_build, :failed, name: 'build', stage_idx: 0, pipeline: pipeline) create(:ci_build, :canceled, name: 'jenkins', stage_idx: 1, pipeline: pipeline) - pipeline.retry_failed(create(:user)) + pipeline.retry_failed(user) end it 'retries both builds' do - expect(latest_status).to contain_exactly('pending', 'pending') + expect(latest_status).to contain_exactly('pending', 'created') end end end diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index 3f32248e52b..76ce558eea0 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -113,7 +113,7 @@ describe Ci::Runner, models: true do context 'when runner has tags' do before do - runner.tag_list = ['bb', 'cc'] + runner.tag_list = %w(bb cc) end shared_examples 'tagged build picker' do @@ -169,7 +169,7 @@ describe Ci::Runner, models: true do context 'when having runner tags' do before do - runner.tag_list = ['bb', 'cc'] + runner.tag_list = %w(bb cc) end it 'cannot handle it for builds without matching tags' do @@ -189,7 +189,7 @@ describe Ci::Runner, models: true do context 'when having runner tags' do before do - runner.tag_list = ['bb', 'cc'] + runner.tag_list = %w(bb cc) build.tag_list = ['bb'] end @@ -212,7 +212,7 @@ describe Ci::Runner, models: true do context 'when having runner tags' do before do - runner.tag_list = ['bb', 'cc'] + runner.tag_list = %w(bb cc) build.tag_list = ['bb'] end @@ -290,7 +290,7 @@ describe Ci::Runner, models: true do let!(:last_update) { runner.ensure_runner_queue_value } before do - runner.update(description: 'new runner') + Ci::UpdateRunnerService.new(runner).update(description: 'new runner') end it 'sets a new last_update value' do @@ -318,6 +318,25 @@ describe Ci::Runner, models: true do end end + describe '#destroy' do + let(:runner) { create(:ci_runner) } + + context 'when there is a tick in the queue' do + let!(:queue_key) { runner.send(:runner_queue_key) } + + before do + runner.tick_runner_queue + runner.destroy + end + + it 'cleans up the queue' do + Gitlab::Redis.with do |redis| + expect(redis.get(queue_key)).to be_nil + end + end + end + end + describe '.assignable_for' do let(:runner) { create(:ci_runner) } let(:project) { create(:empty_project) } diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index bf4394f7d5b..36533bdd11e 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe CommitStatus, models: true do +describe CommitStatus, :models do let(:project) { create(:project, :repository) } let(:pipeline) do @@ -127,7 +127,7 @@ describe CommitStatus, models: true do end describe '.latest' do - subject { CommitStatus.latest.order(:id) } + subject { described_class.latest.order(:id) } let(:statuses) do [create_status(name: 'aa', ref: 'bb', status: 'running'), @@ -143,7 +143,7 @@ describe CommitStatus, models: true do end describe '.running_or_pending' do - subject { CommitStatus.running_or_pending.order(:id) } + subject { described_class.running_or_pending.order(:id) } let(:statuses) do [create_status(name: 'aa', ref: 'bb', status: 'running'), @@ -159,7 +159,21 @@ describe CommitStatus, models: true do end describe '.exclude_ignored' do - subject { CommitStatus.exclude_ignored.order(:id) } + subject { described_class.after_stage(0) } + + let(:statuses) do + [create_status(name: 'aa', stage_idx: 0), + create_status(name: 'cc', stage_idx: 1), + create_status(name: 'aa', stage_idx: 2)] + end + + it 'returns statuses from second and third stage' do + is_expected.to eq(statuses.values_at(1, 2)) + end + end + + describe '.exclude_ignored' do + subject { described_class.exclude_ignored.order(:id) } let(:statuses) do [create_status(when: 'manual', status: 'skipped'), diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb index 2e3702f7520..6151d53cd91 100644 --- a/spec/models/concerns/cache_markdown_field_spec.rb +++ b/spec/models/concerns/cache_markdown_field_spec.rb @@ -1,7 +1,8 @@ require 'spec_helper' describe CacheMarkdownField do - CacheMarkdownField::CACHING_CLASSES << "ThingWithMarkdownFields" + caching_classes = CacheMarkdownField::CACHING_CLASSES + CacheMarkdownField::CACHING_CLASSES = ["ThingWithMarkdownFields"].freeze # The minimum necessary ActiveModel to test this concern class ThingWithMarkdownFields @@ -54,7 +55,7 @@ describe CacheMarkdownField do end end - CacheMarkdownField::CACHING_CLASSES.delete("ThingWithMarkdownFields") + CacheMarkdownField::CACHING_CLASSES = caching_classes def thing_subclass(new_attr) Class.new(ThingWithMarkdownFields) { add_attr(new_attr) } diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb index e008ec28fa4..677e60e1282 100644 --- a/spec/models/concerns/routable_spec.rb +++ b/spec/models/concerns/routable_spec.rb @@ -86,7 +86,7 @@ describe Group, 'Routable' do let(:nested_group) { create(:group, parent: group) } it { expect(group.full_path).to eq(group.path) } - it { expect(nested_group.full_path).to eq("#{group.path}/#{nested_group.path}") } + it { expect(nested_group.full_path).to eq("#{group.full_path}/#{nested_group.path}") } end describe '#full_name' do @@ -102,7 +102,7 @@ describe Project, 'Routable' do describe '#full_path' do let(:project) { build_stubbed(:empty_project) } - it { expect(project.full_path).to eq "#{project.namespace.path}/#{project.path}" } + it { expect(project.full_path).to eq "#{project.namespace.full_path}/#{project.path}" } end describe '#full_name' do diff --git a/spec/models/concerns/spammable_spec.rb b/spec/models/concerns/spammable_spec.rb index 32935bc0b09..fd3b8307571 100644 --- a/spec/models/concerns/spammable_spec.rb +++ b/spec/models/concerns/spammable_spec.rb @@ -14,14 +14,16 @@ describe Issue, 'Spammable' do end describe 'InstanceMethods' do + let(:issue) { build(:issue, spam: true) } + it 'should be invalid if spam' do - issue = build(:issue, spam: true) expect(issue.valid?).to be_falsey end describe '#check_for_spam?' do it 'returns true for public project' do issue.project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC) + expect(issue.check_for_spam?).to eq(true) end @@ -29,5 +31,20 @@ describe Issue, 'Spammable' do expect(issue.check_for_spam?).to eq(false) end end + + describe '#submittable_as_spam_by?' do + let(:admin) { build(:admin) } + let(:user) { build(:user) } + + before do + allow(issue).to receive(:submittable_as_spam?).and_return(true) + end + + it 'tests if the user can submit spam' do + expect(issue.submittable_as_spam_by?(admin)).to be(true) + expect(issue.submittable_as_spam_by?(user)).to be(false) + expect(issue.submittable_as_spam_by?(nil)).to be_nil + end + end end end diff --git a/spec/models/concerns/uniquify_spec.rb b/spec/models/concerns/uniquify_spec.rb new file mode 100644 index 00000000000..83187d732e4 --- /dev/null +++ b/spec/models/concerns/uniquify_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe Uniquify, models: true do + let(:uniquify) { described_class.new } + + describe "#string" do + it 'returns the given string if it does not exist' do + result = uniquify.string('test_string') { |s| false } + + expect(result).to eq('test_string') + end + + it 'returns the given string with a counter attached if the string exists' do + result = uniquify.string('test_string') { |s| s == 'test_string' } + + expect(result).to eq('test_string1') + end + + it 'increments the counter for each candidate string that also exists' do + result = uniquify.string('test_string') { |s| s == 'test_string' || s == 'test_string1' } + + expect(result).to eq('test_string2') + end + + it 'allows passing in a base function that defines the location of the counter' do + result = uniquify.string(-> (counter) { "test_#{counter}_string" }) do |s| + s == 'test__string' + end + + expect(result).to eq('test_1_string') + end + end +end diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb index b9fe492fe2c..e6a826a9418 100644 --- a/spec/models/cycle_analytics/production_spec.rb +++ b/spec/models/cycle_analytics/production_spec.rb @@ -21,13 +21,12 @@ describe 'CycleAnalytics#production', feature: true do ["production deploy happens after merge request is merged (along with other changes)", lambda do |context, data| # Make other changes on master - sha = context.project.repository.commit_file( + sha = context.project.repository.create_file( context.user, context.random_git_name, 'content', message: 'commit message', - branch_name: 'master', - update: false) + branch_name: 'master') context.project.repository.commit(sha) context.deploy_master diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb index 9a024d533a1..3a02ed81adb 100644 --- a/spec/models/cycle_analytics/staging_spec.rb +++ b/spec/models/cycle_analytics/staging_spec.rb @@ -18,7 +18,7 @@ describe 'CycleAnalytics#staging', feature: true do start_time_conditions: [["merge request that closes issue is merged", -> (context, data) do context.merge_merge_requests_closing_issue(data[:issue]) - end ]], + end]], end_time_conditions: [["merge request that closes issue is deployed to production", -> (context, data) do context.deploy_master @@ -26,13 +26,12 @@ describe 'CycleAnalytics#staging', feature: true do ["production deploy happens after merge request is merged (along with other changes)", lambda do |context, data| # Make other changes on master - sha = context.project.repository.commit_file( + sha = context.project.repository.create_file( context.user, context.random_git_name, 'content', message: 'commit message', - branch_name: 'master', - update: false) + branch_name: 'master') context.project.repository.commit(sha) context.deploy_master diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 960f29f3805..dce18f008f8 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -76,7 +76,8 @@ describe Environment, models: true do end describe '#update_merge_request_metrics?' do - { 'production' => true, + { + 'production' => true, 'production/eu' => true, 'production/www.gitlab.com' => true, 'productioneu' => false, @@ -155,7 +156,7 @@ describe Environment, models: true do end describe '#stop_with_action!' do - let(:user) { create(:user) } + let(:user) { create(:admin) } subject { environment.stop_with_action!(user) } @@ -311,7 +312,7 @@ describe Environment, models: true do end describe '#generate_slug' do - SUFFIX = "-[a-z0-9]{6}" + SUFFIX = "-[a-z0-9]{6}".freeze { "staging-12345678901234567" => "staging-123456789" + SUFFIX, "9-staging-123456789012345" => "env-9-staging-123" + SUFFIX, diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index 16e2144d6a1..c720cc9f2c2 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -129,6 +129,14 @@ describe Member, models: true do it { expect(described_class.request).not_to include @accepted_request_member } end + describe '.non_request' do + it { expect(described_class.non_request).to include @master } + it { expect(described_class.non_request).to include @invited_member } + it { expect(described_class.non_request).to include @accepted_invite_member } + it { expect(described_class.non_request).not_to include @requested_member } + it { expect(described_class.non_request).to include @accepted_request_member } + end + describe '.developers' do subject { described_class.developers.to_a } diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb index e4be0aba7a6..87ea2e70680 100644 --- a/spec/models/members/project_member_spec.rb +++ b/spec/models/members/project_member_spec.rb @@ -89,8 +89,8 @@ describe ProjectMember, models: true do @user_1 = create :user @user_2 = create :user - @project_1.team << [ @user_1, :developer ] - @project_2.team << [ @user_2, :reporter ] + @project_1.team << [@user_1, :developer] + @project_2.team << [@user_2, :reporter] @status = @project_2.team.import(@project_1) end @@ -137,8 +137,8 @@ describe ProjectMember, models: true do @user_1 = create :user @user_2 = create :user - @project_1.team << [ @user_1, :developer] - @project_2.team << [ @user_2, :reporter] + @project_1.team << [@user_1, :developer] + @project_2.team << [@user_2, :reporter] ProjectMember.truncate_teams([@project_1.id, @project_2.id]) end diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb index 6d599e148a2..0a10ee01506 100644 --- a/spec/models/merge_request_diff_spec.rb +++ b/spec/models/merge_request_diff_spec.rb @@ -109,7 +109,7 @@ describe MergeRequestDiff, models: true do { id: 'sha2' } ] - expect(subject.commits_sha).to eq(['sha1', 'sha2']) + expect(subject.commits_sha).to eq(%w(sha1 sha2)) end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index a01741a9971..e000d0d38b3 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -37,12 +37,12 @@ describe MergeRequest, models: true do end it "is invalid without merge user" do - subject.merge_when_build_succeeds = true + subject.merge_when_pipeline_succeeds = true expect(subject).not_to be_valid end it "is valid with merge user" do - subject.merge_when_build_succeeds = true + subject.merge_when_pipeline_succeeds = true subject.merge_user = build(:user) expect(subject).to be_valid @@ -55,7 +55,7 @@ describe MergeRequest, models: true do it { is_expected.to respond_to(:can_be_merged?) } it { is_expected.to respond_to(:cannot_be_merged?) } it { is_expected.to respond_to(:merge_params) } - it { is_expected.to respond_to(:merge_when_build_succeeds) } + it { is_expected.to respond_to(:merge_when_pipeline_succeeds) } end describe '.in_projects' do @@ -209,6 +209,50 @@ describe MergeRequest, models: true do end end + describe '#diff_size' do + let(:merge_request) do + build(:merge_request, source_branch: 'expand-collapse-files', target_branch: 'master') + end + + context 'when there are MR diffs' do + before do + merge_request.save + end + + it 'returns the correct count' do + expect(merge_request.diff_size).to eq(105) + end + + it 'does not perform highlighting' do + expect(Gitlab::Diff::Highlight).not_to receive(:new) + + merge_request.diff_size + end + end + + context 'when there are no MR diffs' do + before do + merge_request.compare = CompareService.new( + merge_request.source_project, + merge_request.source_branch + ).execute( + merge_request.target_project, + merge_request.target_branch + ) + end + + it 'returns the correct count' do + expect(merge_request.diff_size).to eq(105) + end + + it 'does not perform highlighting' do + expect(Gitlab::Diff::Highlight).not_to receive(:new) + + merge_request.diff_size + end + end + end + describe "#related_notes" do let!(:merge_request) { create(:merge_request) } @@ -464,17 +508,17 @@ describe MergeRequest, models: true do end end - describe "#reset_merge_when_build_succeeds" do + describe "#reset_merge_when_pipeline_succeeds" do let(:merge_if_green) do - create :merge_request, merge_when_build_succeeds: true, merge_user: create(:user), + create :merge_request, merge_when_pipeline_succeeds: true, merge_user: create(:user), merge_params: { "should_remove_source_branch" => "1", "commit_message" => "msg" } end it "sets the item to false" do - merge_if_green.reset_merge_when_build_succeeds + merge_if_green.reset_merge_when_pipeline_succeeds merge_if_green.reload - expect(merge_if_green.merge_when_build_succeeds).to be_falsey + expect(merge_if_green.merge_when_pipeline_succeeds).to be_falsey expect(merge_if_green.merge_params["should_remove_source_branch"]).to be_nil expect(merge_if_green.merge_params["commit_message"]).to be_nil end @@ -768,7 +812,7 @@ describe MergeRequest, models: true do end describe '#check_if_can_be_merged' do - let(:project) { create(:empty_project, only_allow_merge_if_build_succeeds: true) } + let(:project) { create(:empty_project, only_allow_merge_if_pipeline_succeeds: true) } subject { create(:merge_request, source_project: project, merge_status: :unchecked) } @@ -789,12 +833,6 @@ describe MergeRequest, models: true do it 'becomes unmergeable' do expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged') end - - it 'creates Todo on unmergeability' do - expect_any_instance_of(TodoService).to receive(:merge_request_became_unmergeable).with(subject) - - subject.check_if_can_be_merged - end end context 'when it has conflicts' do @@ -889,7 +927,7 @@ describe MergeRequest, models: true do end describe '#mergeable_ci_state?' do - let(:project) { create(:empty_project, only_allow_merge_if_build_succeeds: true) } + let(:project) { create(:empty_project, only_allow_merge_if_pipeline_succeeds: true) } let(:pipeline) { create(:ci_empty_pipeline) } subject { build(:merge_request, target_project: project) } @@ -932,7 +970,7 @@ describe MergeRequest, models: true do end context 'when merges are not restricted to green builds' do - subject { build(:merge_request, target_project: build(:empty_project, only_allow_merge_if_build_succeeds: false)) } + subject { build(:merge_request, target_project: build(:empty_project, only_allow_merge_if_pipeline_succeeds: false)) } context 'and a failed pipeline is associated' do before do @@ -1537,7 +1575,7 @@ describe MergeRequest, models: true do status: status) end - let(:project) { create(:project, :public, :repository, only_allow_merge_if_build_succeeds: true) } + let(:project) { create(:project, :public, :repository, only_allow_merge_if_pipeline_succeeds: true) } let(:developer) { create(:user) } let(:user) { create(:user) } let(:merge_request) { create(:merge_request, source_project: project) } diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 35d932f1c64..3f9c4289de9 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -36,7 +36,7 @@ describe Namespace, models: true do end describe '#to_param' do - it { expect(namespace.to_param).to eq(namespace.path) } + it { expect(namespace.to_param).to eq(namespace.full_path) } end describe '#human_name' do @@ -151,7 +151,7 @@ describe Namespace, models: true do describe :rm_dir do let!(:project) { create(:empty_project, namespace: namespace) } - let!(:path) { File.join(Gitlab.config.repositories.storages.default, namespace.path) } + let!(:path) { File.join(Gitlab.config.repositories.storages.default, namespace.full_path) } it "removes its dirs when deleted" do namespace.destroy diff --git a/spec/models/project_group_link_spec.rb b/spec/models/project_group_link_spec.rb index 59a4ae1b799..9b711bfc007 100644 --- a/spec/models/project_group_link_spec.rb +++ b/spec/models/project_group_link_spec.rb @@ -7,12 +7,27 @@ describe ProjectGroupLink do end describe "Validation" do - let!(:project_group_link) { create(:project_group_link) } + let(:parent_group) { create(:group) } + let(:group) { create(:group, parent: parent_group) } + let(:project) { create(:project, group: group) } + let!(:project_group_link) { create(:project_group_link, project: project) } it { should validate_presence_of(:project_id) } it { should validate_uniqueness_of(:group_id).scoped_to(:project_id).with_message(/already shared/) } it { should validate_presence_of(:group) } it { should validate_presence_of(:group_access) } + + it "doesn't allow a project to be shared with the group it is in" do + project_group_link.group = group + + expect(project_group_link).not_to be_valid + end + + it "doesn't allow a project to be shared with an ancestor of the group it is in" do + project_group_link.group = parent_group + + expect(project_group_link).not_to be_valid + end end describe "destroying a record", truncate: true do diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb index 497a626a418..4014d6129ee 100644 --- a/spec/models/project_services/bamboo_service_spec.rb +++ b/spec/models/project_services/bamboo_service_spec.rb @@ -181,7 +181,7 @@ describe BambooService, models: true, caching: true do end it 'sets commit status to "pending" when response has no results' do - stub_request(body: %Q({"results":{"results":{"size":"0"}}})) + stub_request(body: %q({"results":{"results":{"size":"0"}}})) is_expected.to eq('pending') end diff --git a/spec/models/project_services/buildkite_service_spec.rb b/spec/models/project_services/buildkite_service_spec.rb index dbd23ff5491..05b602d8106 100644 --- a/spec/models/project_services/buildkite_service_spec.rb +++ b/spec/models/project_services/buildkite_service_spec.rb @@ -92,7 +92,7 @@ describe BuildkiteService, models: true, caching: true do end it 'passes through build status untouched when status is 200' do - stub_request(body: %Q({"status":"Great Success"})) + stub_request(body: %q({"status":"Great Success"})) is_expected.to eq('Great Success') end @@ -101,7 +101,7 @@ describe BuildkiteService, models: true, caching: true do end def stub_request(status: 200, body: nil) - body ||= %Q({"status":"success"}) + body ||= %q({"status":"success"}) buildkite_full_url = 'https://gitlab.buildkite.com/status/secret-sauce-status-token.json?commit=123' WebMock.stub_request(:get, buildkite_full_url).to_return( diff --git a/spec/models/project_services/chat_message/build_message_spec.rb b/spec/models/project_services/chat_message/build_message_spec.rb index 50ad5013df9..3bd7ec18ae0 100644 --- a/spec/models/project_services/chat_message/build_message_spec.rb +++ b/spec/models/project_services/chat_message/build_message_spec.rb @@ -11,21 +11,28 @@ describe ChatMessage::BuildMessage do project_name: 'project_name', project_url: 'http://example.gitlab.com', + build_id: 1, + build_name: build_name, + build_stage: stage, commit: { status: status, author_name: 'hacker', + author_url: 'http://example.gitlab.com/hacker', duration: duration, }, } end let(:message) { build_message } + let(:stage) { 'test' } + let(:status) { 'success' } + let(:build_name) { 'rspec' } + let(:duration) { 10 } 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 @@ -38,7 +45,6 @@ describe ChatMessage::BuildMessage do context 'build 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 @@ -47,11 +53,25 @@ describe ChatMessage::BuildMessage do end end - def build_message(status_text = status) + it 'returns a message with information on build' do + expect(subject.fallback).to include("on build <http://example.gitlab.com/builds/1|#{build_name}>") + end + + it 'returns a message with stage name' do + expect(subject.fallback).to include("of stage #{stage}") + end + + it 'returns a message with link to author' do + expect(subject.fallback).to include("by <http://example.gitlab.com/hacker|hacker>") + end + + def build_message(status_text = status, stage_text = stage, build_text = build_name) "<http://example.gitlab.com|project_name>:" \ " Commit <http://example.gitlab.com/commit/" \ "97de212e80737a608d939f648d959671fb0a0142/builds|97de212e>" \ " of <http://example.gitlab.com/commits/develop|develop> branch" \ - " by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}" + " by <http://example.gitlab.com/hacker|hacker> #{status_text}" \ + " on build <http://example.gitlab.com/builds/1|#{build_text}>" \ + " of stage #{stage_text} in #{duration} #{'second'.pluralize(duration)}" end end diff --git a/spec/models/project_services/drone_ci_service_spec.rb b/spec/models/project_services/drone_ci_service_spec.rb index f9307d6de7b..044737c6026 100644 --- a/spec/models/project_services/drone_ci_service_spec.rb +++ b/spec/models/project_services/drone_ci_service_spec.rb @@ -28,7 +28,7 @@ describe DroneCiService, models: true, caching: true do shared_context :drone_ci_service do let(:drone) { DroneCiService.new } let(:project) { create(:project, :repository, name: 'project') } - let(:path) { "#{project.namespace.path}/#{project.path}" } + let(:path) { project.full_path } let(:drone_url) { 'http://drone.example.com' } let(:sha) { '2ab7834c' } let(:branch) { 'dev' } @@ -50,7 +50,7 @@ describe DroneCiService, models: true, caching: true do end def stub_request(status: 200, body: nil) - body ||= %Q({"status":"success"}) + body ||= %q({"status":"success"}) WebMock.stub_request(:get, commit_status_path).to_return( status: status, @@ -95,12 +95,12 @@ describe DroneCiService, models: true, caching: true do is_expected.to eq(:error) end - { "killed" => :canceled, + { + "killed" => :canceled, "failure" => :failed, "error" => :failed, - "success" => "success", + "success" => "success" }.each do |drone_status, our_status| - it "sets commit status to #{our_status.inspect} when returned status is #{drone_status.inspect}" do stub_request(body: %Q({"status":"#{drone_status}"})) diff --git a/spec/models/project_services/irker_service_spec.rb b/spec/models/project_services/irker_service_spec.rb index b9fb6f3f6f4..d5a16226d9d 100644 --- a/spec/models/project_services/irker_service_spec.rb +++ b/spec/models/project_services/irker_service_spec.rb @@ -59,8 +59,8 @@ describe IrkerService, models: true do conn = @irker_server.accept conn.readlines.each do |line| - msg = JSON.load(line.chomp("\n")) - expect(msg.keys).to match_array(['to', 'privmsg']) + msg = JSON.parse(line.chomp("\n")) + expect(msg.keys).to match_array(%w(to privmsg)) expect(msg['to']).to match_array(["irc://chat.freenode.net/#commits", "irc://test.net/#test"]) end diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index 9052479d35e..585c899cdf9 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -163,6 +163,12 @@ describe KubernetesService, models: true, caching: true do { key: 'KUBE_CA_PEM', value: 'CA PEM DATA', public: true } ) end + + it 'sets KUBE_CA_PEM_FILE' do + expect(subject.predefined_variables).to include( + { key: 'KUBE_CA_PEM_FILE', value: 'CA PEM DATA', public: true, file: true } + ) + end end describe '#terminals' do @@ -171,7 +177,7 @@ describe KubernetesService, models: true, caching: true do context 'with invalid pods' do it 'returns no terminals' do - stub_reactive_cache(service, pods: [ { "bad" => "pod" } ]) + stub_reactive_cache(service, pods: [{ "bad" => "pod" }]) is_expected.to be_empty end @@ -184,7 +190,7 @@ describe KubernetesService, models: true, caching: true do before do stub_reactive_cache( service, - pods: [ pod, pod, kube_pod(app: "should-be-filtered-out") ] + pods: [pod, pod, kube_pod(app: "should-be-filtered-out")] ) end diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/project_services/mattermost_slash_commands_service_spec.rb index 98f3d420c8a..598ff232c82 100644 --- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb +++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb @@ -36,7 +36,8 @@ describe MattermostSlashCommandsService, :models do description: "Perform common operations on: #{project.name_with_namespace}", display_name: "GitLab / #{project.name_with_namespace}", method: 'P', - username: 'GitLab' }.to_json). + username: 'GitLab' + }.to_json). to_return( status: 200, headers: { 'Content-Type' => 'application/json' }, diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb index a1edd083aa1..77b18e1c7d0 100644 --- a/spec/models/project_services/teamcity_service_spec.rb +++ b/spec/models/project_services/teamcity_service_spec.rb @@ -143,7 +143,7 @@ describe TeamcityService, models: true, caching: true do end it 'returns a build URL when teamcity_url has no trailing slash' do - stub_request(body: %Q({"build":{"id":"666"}})) + stub_request(body: %q({"build":{"id":"666"}})) is_expected.to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo') end @@ -152,7 +152,7 @@ describe TeamcityService, models: true, caching: true do let(:teamcity_url) { 'http://gitlab.com/teamcity/' } it 'returns a build URL' do - stub_request(body: %Q({"build":{"id":"666"}})) + stub_request(body: %q({"build":{"id":"666"}})) is_expected.to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo') end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 35f3dd00870..ee4f4092062 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -402,7 +402,7 @@ describe Project, models: true do let(:project) { create(:empty_project, path: "somewhere") } it 'returns the full web URL for this repo' do - expect(project.web_url).to eq("#{Gitlab.config.gitlab.url}/#{project.namespace.path}/somewhere") + expect(project.web_url).to eq("#{Gitlab.config.gitlab.url}/#{project.namespace.full_path}/somewhere") end end @@ -803,7 +803,7 @@ describe Project, models: true do end let(:avatar_path) do - "/#{project.namespace.name}/#{project.path}/avatar" + "/#{project.full_path}/avatar" end it { should eq "http://#{Gitlab.config.gitlab.host}#{avatar_path}" } @@ -1148,16 +1148,14 @@ describe Project, models: true do end it 'renames a repository' do - ns = project.namespace_dir - expect(gitlab_shell).to receive(:mv_repository). ordered. - with(project.repository_storage_path, "#{ns}/foo", "#{ns}/#{project.path}"). + with(project.repository_storage_path, "#{project.namespace.full_path}/foo", "#{project.full_path}"). and_return(true) expect(gitlab_shell).to receive(:mv_repository). ordered. - with(project.repository_storage_path, "#{ns}/foo.wiki", "#{ns}/#{project.path}.wiki"). + with(project.repository_storage_path, "#{project.namespace.full_path}/foo.wiki", "#{project.full_path}.wiki"). and_return(true) expect_any_instance_of(SystemHooksService). @@ -1166,7 +1164,7 @@ describe Project, models: true do expect_any_instance_of(Gitlab::UploadsTransfer). to receive(:rename_project). - with('foo', project.path, ns) + with('foo', project.path, project.namespace.full_path) expect(project).to receive(:expire_caches_before_rename) @@ -1538,7 +1536,7 @@ describe Project, models: true do it 'schedules a RepositoryForkWorker job' do expect(RepositoryForkWorker).to receive(:perform_async). with(project.id, forked_from_project.repository_storage_path, - forked_from_project.path_with_namespace, project.namespace.path) + forked_from_project.path_with_namespace, project.namespace.full_path) project.add_import_job end @@ -1752,7 +1750,7 @@ describe Project, models: true do describe 'inside_path' do let!(:project1) { create(:empty_project) } let!(:project2) { create(:empty_project) } - let!(:path) { project1.namespace.path } + let!(:path) { project1.namespace.full_path } it { expect(Project.inside_path(path)).to eq([project1]) } end @@ -1767,7 +1765,7 @@ describe Project, models: true do end before do - project.repository.commit_file(User.last, '.gitlab/route-map.yml', route_map, message: 'Add .gitlab/route-map.yml', branch_name: 'master', update: false) + project.repository.create_file(User.last, '.gitlab/route-map.yml', route_map, message: 'Add .gitlab/route-map.yml', branch_name: 'master') end context 'when there is a .gitlab/route-map.yml at the commit' do @@ -1852,8 +1850,8 @@ describe Project, models: true do end describe '#pages_url' do - let(:group) { create :group, name: group_name } - let(:project) { create :empty_project, namespace: group, name: project_name } + let(:group) { create :group, name: 'Group' } + let(:nested_group) { create :group, parent: group } let(:domain) { 'Example.com' } subject { project.pages_url } @@ -1863,18 +1861,58 @@ describe Project, models: true do allow(Gitlab.config.pages).to receive(:url).and_return('http://example.com') end - context 'group page' do - let(:group_name) { 'Group' } - let(:project_name) { 'group.example.com' } + context 'top-level group' do + let(:project) { create :empty_project, namespace: group, name: project_name } + + context 'group page' do + let(:project_name) { 'group.example.com' } + + it { is_expected.to eq("http://group.example.com") } + end + + context 'project page' do + let(:project_name) { 'Project' } + + it { is_expected.to eq("http://group.example.com/project") } + end + end + + context 'nested group' do + let(:project) { create :empty_project, namespace: nested_group, name: project_name } + let(:expected_url) { "http://group.example.com/#{nested_group.path}/#{project.path}" } + + context 'group page' do + let(:project_name) { 'group.example.com' } + + it { is_expected.to eq(expected_url) } + end + + context 'project page' do + let(:project_name) { 'Project' } + + it { is_expected.to eq(expected_url) } + end + end + end + + describe '#http_url_to_repo' do + let(:project) { create :empty_project } + + context 'when no user is given' do + it 'returns the url to the repo without a username' do + url = project.http_url_to_repo - it { is_expected.to eq("http://group.example.com") } + expect(url).to eq(project.http_url_to_repo) + expect(url).not_to include('@') + end end - context 'project page' do - let(:group_name) { 'Group' } - let(:project_name) { 'Project' } + context 'when user is given' do + it 'returns the url to the repo with the username' do + user = build_stubbed(:user) - it { is_expected.to eq("http://group.example.com/project") } + expect(project.http_url_to_repo(user)).to match(%r{https?:\/\/#{user.username}@}) + end end end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 9bfa6409607..eb992e1354e 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -291,10 +291,10 @@ describe Repository, models: true do end end - describe "#commit_dir" do + describe "#create_dir" do it "commits a change that creates a new directory" do expect do - repository.commit_dir(user, 'newdir', + repository.create_dir(user, 'newdir', message: 'Create newdir', branch_name: 'master') end.to change { repository.commits('master').count }.by(1) @@ -307,7 +307,7 @@ describe Repository, models: true do it "creates a fork and commit to the forked project" do expect do - repository.commit_dir(user, 'newdir', + repository.create_dir(user, 'newdir', message: 'Create newdir', branch_name: 'patch', start_branch_name: 'master', start_project: forked_project) end.to change { repository.commits('master').count }.by(0) @@ -323,7 +323,7 @@ describe Repository, models: true do context "when an author is specified" do it "uses the given email/name to set the commit's author" do expect do - repository.commit_dir(user, 'newdir', + repository.create_dir(user, 'newdir', message: 'Add newdir', branch_name: 'master', author_email: author_email, author_name: author_name) @@ -337,27 +337,35 @@ describe Repository, models: true do end end - describe "#commit_file" do - it 'commits change to a file successfully' do + describe "#create_file" do + it 'commits new file successfully' do expect do - repository.commit_file(user, 'CHANGELOG', 'Changelog!', - message: 'Updates file content', - branch_name: 'master', - update: true) + repository.create_file(user, 'NEWCHANGELOG', 'Changelog!', + message: 'Create changelog', + branch_name: 'master') end.to change { repository.commits('master').count }.by(1) - blob = repository.blob_at('master', 'CHANGELOG') + blob = repository.blob_at('master', 'NEWCHANGELOG') expect(blob.data).to eq('Changelog!') end + it 'respects the autocrlf setting' do + repository.create_file(user, 'hello.txt', "Hello,\r\nWorld", + message: 'Add hello world', + branch_name: 'master') + + blob = repository.blob_at('master', 'hello.txt') + + expect(blob.data).to eq("Hello,\nWorld") + end + context "when an author is specified" do it "uses the given email/name to set the commit's author" do expect do - repository.commit_file(user, 'README', 'README!', + repository.create_file(user, 'NEWREADME', 'README!', message: 'Add README', branch_name: 'master', - update: true, author_email: author_email, author_name: author_name) end.to change { repository.commits('master').count }.by(1) @@ -371,6 +379,18 @@ describe Repository, models: true do end describe "#update_file" do + it 'updates file successfully' do + expect do + repository.update_file(user, 'CHANGELOG', 'Changelog!', + message: 'Update changelog', + branch_name: 'master') + end.to change { repository.commits('master').count }.by(1) + + blob = repository.blob_at('master', 'CHANGELOG') + + expect(blob.data).to eq('Changelog!') + end + it 'updates filename successfully' do expect do repository.update_file(user, 'NEWLICENSE', 'Copyright!', @@ -387,9 +407,6 @@ describe Repository, models: true do context "when an author is specified" do it "uses the given email/name to set the commit's author" do - repository.commit_file(user, 'README', 'README!', - message: 'Add README', branch_name: 'master', update: true) - expect do repository.update_file(user, 'README', 'Updated README!', branch_name: 'master', @@ -407,13 +424,10 @@ describe Repository, models: true do end end - describe "#remove_file" do + describe "#delete_file" do it 'removes file successfully' do - repository.commit_file(user, 'README', 'README!', - message: 'Add README', branch_name: 'master', update: true) - expect do - repository.remove_file(user, 'README', + repository.delete_file(user, 'README', message: 'Remove README', branch_name: 'master') end.to change { repository.commits('master').count }.by(1) @@ -422,11 +436,8 @@ describe Repository, models: true do context "when an author is specified" do it "uses the given email/name to set the commit's author" do - repository.commit_file(user, 'README', 'README!', - message: 'Add README', branch_name: 'master', update: true) - expect do - repository.remove_file(user, 'README', + repository.delete_file(user, 'README', message: 'Remove README', branch_name: 'master', author_email: author_email, author_name: author_name) end.to change { repository.commits('master').count }.by(1) @@ -576,14 +587,14 @@ describe Repository, models: true do describe "#license_blob", caching: true do before do - repository.remove_file( + repository.delete_file( user, 'LICENSE', message: 'Remove LICENSE', branch_name: 'master') end it 'handles when HEAD points to non-existent ref' do - repository.commit_file( + repository.create_file( user, 'LICENSE', 'Copyright!', - message: 'Add LICENSE', branch_name: 'master', update: false) + message: 'Add LICENSE', branch_name: 'master') allow(repository).to receive(:file_on_head). and_raise(Rugged::ReferenceError) @@ -592,27 +603,27 @@ describe Repository, models: true do end it 'looks in the root_ref only' do - repository.remove_file(user, 'LICENSE', + repository.delete_file(user, 'LICENSE', message: 'Remove LICENSE', branch_name: 'markdown') - repository.commit_file(user, 'LICENSE', + repository.create_file(user, 'LICENSE', Licensee::License.new('mit').content, - message: 'Add LICENSE', branch_name: 'markdown', update: false) + message: 'Add LICENSE', branch_name: 'markdown') expect(repository.license_blob).to be_nil end it 'detects license file with no recognizable open-source license content' do - repository.commit_file(user, 'LICENSE', 'Copyright!', - message: 'Add LICENSE', branch_name: 'master', update: false) + repository.create_file(user, 'LICENSE', 'Copyright!', + message: 'Add LICENSE', branch_name: 'master') expect(repository.license_blob.name).to eq('LICENSE') end %w[LICENSE LICENCE LiCensE LICENSE.md LICENSE.foo COPYING COPYING.md].each do |filename| it "detects '#{filename}'" do - repository.commit_file(user, filename, + repository.create_file(user, filename, Licensee::License.new('mit').content, - message: "Add #{filename}", branch_name: 'master', update: false) + message: "Add #{filename}", branch_name: 'master') expect(repository.license_blob.name).to eq(filename) end @@ -621,7 +632,7 @@ describe Repository, models: true do describe '#license_key', caching: true do before do - repository.remove_file(user, 'LICENSE', + repository.delete_file(user, 'LICENSE', message: 'Remove LICENSE', branch_name: 'master') end @@ -636,16 +647,16 @@ describe Repository, models: true do end it 'detects license file with no recognizable open-source license content' do - repository.commit_file(user, 'LICENSE', 'Copyright!', - message: 'Add LICENSE', branch_name: 'master', update: false) + repository.create_file(user, 'LICENSE', 'Copyright!', + message: 'Add LICENSE', branch_name: 'master') expect(repository.license_key).to be_nil end it 'returns the license key' do - repository.commit_file(user, 'LICENSE', + repository.create_file(user, 'LICENSE', Licensee::License.new('mit').content, - message: 'Add LICENSE', branch_name: 'master', update: false) + message: 'Add LICENSE', branch_name: 'master') expect(repository.license_key).to eq('mit') end @@ -902,10 +913,9 @@ describe Repository, models: true do expect(empty_repository).to receive(:expire_emptiness_caches) expect(empty_repository).to receive(:expire_branches_cache) - empty_repository.commit_file(user, 'CHANGELOG', 'Changelog!', + empty_repository.create_file(user, 'CHANGELOG', 'Changelog!', message: 'Updates file content', - branch_name: 'master', - update: false) + branch_name: 'master') end end end @@ -1102,16 +1112,16 @@ describe Repository, models: true do let(:update_image_commit) { repository.commit('2f63565e7aac07bcdadb654e253078b727143ec4') } context 'when there is a conflict' do - it 'aborts the operation' do - expect(repository.revert(user, new_image_commit, 'master')).to eq(false) + it 'raises an error' do + expect { repository.revert(user, new_image_commit, 'master') }.to raise_error(/Failed to/) end end context 'when commit was already reverted' do - it 'aborts the operation' do + it 'raises an error' do repository.revert(user, update_image_commit, 'master') - expect(repository.revert(user, update_image_commit, 'master')).to eq(false) + expect { repository.revert(user, update_image_commit, 'master') }.to raise_error(/Failed to/) end end @@ -1138,16 +1148,16 @@ describe Repository, models: true do let(:pickable_merge) { repository.commit('e56497bb5f03a90a51293fc6d516788730953899') } context 'when there is a conflict' do - it 'aborts the operation' do - expect(repository.cherry_pick(user, conflict_commit, 'master')).to eq(false) + it 'raises an error' do + expect { repository.cherry_pick(user, conflict_commit, 'master') }.to raise_error(/Failed to/) end end context 'when commit was already cherry-picked' do - it 'aborts the operation' do + it 'raises an error' do repository.cherry_pick(user, pickable_commit, 'master') - expect(repository.cherry_pick(user, pickable_commit, 'master')).to eq(false) + expect { repository.cherry_pick(user, pickable_commit, 'master') }.to raise_error(/Failed to/) end end @@ -1371,13 +1381,13 @@ describe Repository, models: true do describe '#branch_count' do it 'returns the number of branches' do - expect(repository.branch_count).to be_an_instance_of(Fixnum) + expect(repository.branch_count).to be_an(Integer) end end describe '#tag_count' do it 'returns the number of tags' do - expect(repository.tag_count).to be_an_instance_of(Fixnum) + expect(repository.tag_count).to be_an(Integer) end end @@ -1727,7 +1737,7 @@ describe Repository, models: true do context 'with an existing repository' do it 'returns the commit count' do - expect(repository.commit_count).to be_an_instance_of(Fixnum) + expect(repository.commit_count).to be_an(Integer) end end end @@ -1785,7 +1795,7 @@ describe Repository, models: true do describe '#gitlab_ci_yml_for' do before do - repository.commit_file(User.last, '.gitlab-ci.yml', 'CONTENT', message: 'Add .gitlab-ci.yml', branch_name: 'master', update: false) + repository.create_file(User.last, '.gitlab-ci.yml', 'CONTENT', message: 'Add .gitlab-ci.yml', branch_name: 'master') end context 'when there is a .gitlab-ci.yml at the commit' do @@ -1803,7 +1813,7 @@ describe Repository, models: true do describe '#route_map_for' do before do - repository.commit_file(User.last, '.gitlab/route-map.yml', 'CONTENT', message: 'Add .gitlab/route-map.yml', branch_name: 'master', update: false) + repository.create_file(User.last, '.gitlab/route-map.yml', 'CONTENT', message: 'Add .gitlab/route-map.yml', branch_name: 'master') end context 'when there is a .gitlab/route-map.yml at the commit' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 7fd49c73b37..e86b4a761d9 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -19,9 +19,10 @@ describe User, models: true do it { is_expected.to have_many(:project_members).dependent(:destroy) } it { is_expected.to have_many(:groups) } it { is_expected.to have_many(:keys).dependent(:destroy) } + it { is_expected.to have_many(:deploy_keys).dependent(:destroy) } it { is_expected.to have_many(:events).dependent(:destroy) } it { is_expected.to have_many(:recent_events).class_name('Event') } - it { is_expected.to have_many(:issues).dependent(:destroy) } + it { is_expected.to have_many(:issues).dependent(:restrict_with_exception) } it { is_expected.to have_many(:notes).dependent(:destroy) } it { is_expected.to have_many(:assigned_issues).dependent(:nullify) } it { is_expected.to have_many(:merge_requests).dependent(:destroy) } @@ -207,6 +208,22 @@ describe User, models: true do end end end + + describe 'ghost users' do + it 'does not allow a non-blocked ghost user' do + user = build(:user, :ghost) + user.state = 'active' + + expect(user).to be_invalid + end + + it 'allows a blocked ghost user' do + user = build(:user, :ghost) + user.state = 'blocked' + + expect(user).to be_valid + end + end end describe "scopes" do @@ -303,6 +320,34 @@ describe User, models: true do end end + shared_context 'user keys' do + let(:user) { create(:user) } + let!(:key) { create(:key, user: user) } + let!(:deploy_key) { create(:deploy_key, user: user) } + end + + describe '#keys' do + include_context 'user keys' + + context 'with key and deploy key stored' do + it 'returns stored key, but not deploy_key' do + expect(user.keys).to include key + expect(user.keys).not_to include deploy_key + end + end + end + + describe '#deploy_keys' do + include_context 'user keys' + + context 'with key and deploy key stored' do + it 'returns stored deploy key, but not normal key' do + expect(user.deploy_keys).to include deploy_key + expect(user.deploy_keys).not_to include key + end + end + end + describe '#confirm' do before do allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true) @@ -553,18 +598,16 @@ describe User, models: true do it "applies defaults to user" do expect(user.projects_limit).to eq(Gitlab.config.gitlab.default_projects_limit) expect(user.can_create_group).to eq(Gitlab.config.gitlab.default_can_create_group) - expect(user.theme_id).to eq(Gitlab.config.gitlab.default_theme) expect(user.external).to be_falsey end end describe 'with default overrides' do - let(:user) { User.new(projects_limit: 123, can_create_group: false, can_create_team: true, theme_id: 1) } + let(:user) { User.new(projects_limit: 123, can_create_group: false, can_create_team: true) } it "applies defaults to user" do expect(user.projects_limit).to eq(123) expect(user.can_create_group).to be_falsey - expect(user.theme_id).to eq(1) end end @@ -666,9 +709,7 @@ describe User, models: true do end describe '.search_with_secondary_emails' do - def search_with_secondary_emails(query) - described_class.search_with_secondary_emails(query) - end + delegate :search_with_secondary_emails, to: :described_class let!(:user) { create(:user) } let!(:email) { create(:email) } @@ -1388,7 +1429,7 @@ describe User, models: true do it { expect(user.nested_groups).to eq([nested_group]) } end - describe '#nested_projects' do + describe '#nested_groups_projects' do let!(:user) { create(:user) } let!(:group) { create(:group) } let!(:nested_group) { create(:group, parent: group) } @@ -1403,7 +1444,7 @@ describe User, models: true do other_project.add_developer(create(:user)) end - it { expect(user.nested_projects).to eq([nested_project]) } + it { expect(user.nested_groups_projects).to eq([nested_project]) } end describe '#refresh_authorized_projects', redis: true do @@ -1465,4 +1506,41 @@ describe User, models: true do expect(user.admin).to be true end end + + describe '.ghost' do + it "creates a ghost user if one isn't already present" do + ghost = User.ghost + + expect(ghost).to be_ghost + expect(ghost).to be_persisted + end + + it "does not create a second ghost user if one is already present" do + expect do + User.ghost + User.ghost + end.to change { User.count }.by(1) + expect(User.ghost).to eq(User.ghost) + end + + context "when a regular user exists with the username 'ghost'" do + it "creates a ghost user with a non-conflicting username" do + create(:user, username: 'ghost') + ghost = User.ghost + + expect(ghost).to be_persisted + expect(ghost.username).to eq('ghost1') + end + end + + context "when a regular user exists with the email 'ghost@example.com'" do + it "creates a ghost user with a non-conflicting email" do + create(:user, email: 'ghost@example.com') + ghost = User.ghost + + expect(ghost).to be_persisted + expect(ghost.email).to eq('ghost1@example.com') + end + end + end end diff --git a/spec/models/wiki_directory_spec.rb b/spec/models/wiki_directory_spec.rb new file mode 100644 index 00000000000..1caaa557085 --- /dev/null +++ b/spec/models/wiki_directory_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +RSpec.describe WikiDirectory, models: true do + describe 'validations' do + subject { build(:wiki_directory) } + + it { is_expected.to validate_presence_of(:slug) } + end + + describe '#initialize' do + context 'when there are pages' do + let(:pages) { [build(:wiki_page)] } + let(:directory) { WikiDirectory.new('/path_up_to/dir', pages) } + + it 'sets the slug attribute' do + expect(directory.slug).to eq('/path_up_to/dir') + end + + it 'sets the pages attribute' do + expect(directory.pages).to eq(pages) + end + end + + context 'when there are no pages' do + let(:directory) { WikiDirectory.new('/path_up_to/dir') } + + it 'sets the slug attribute' do + expect(directory.slug).to eq('/path_up_to/dir') + end + + it 'sets the pages attribute to an empty array' do + expect(directory.pages).to eq([]) + end + end + end + + describe '#to_partial_path' do + it 'returns the relative path to the partial to be used' do + directory = build(:wiki_directory) + + expect(directory.to_partial_path).to eq('projects/wikis/wiki_directory') + end + end +end diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index 5c34b1b0a30..753dc938c52 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -7,6 +7,75 @@ describe WikiPage, models: true do subject { WikiPage.new(wiki) } + describe '.group_by_directory' do + context 'when there are no pages' do + it 'returns an empty array' do + expect(WikiPage.group_by_directory(nil)).to eq([]) + expect(WikiPage.group_by_directory([])).to eq([]) + end + end + + context 'when there are pages' do + before do + create_page('dir_1/dir_1_1/page_3', 'content') + create_page('dir_1/page_2', 'content') + create_page('dir_2/page_5', 'content') + create_page('dir_2/page_4', 'content') + create_page('page_1', 'content') + end + let(:page_1) { wiki.find_page('page_1') } + let(:dir_1) do + WikiDirectory.new('dir_1', [wiki.find_page('dir_1/page_2')]) + end + let(:dir_1_1) do + WikiDirectory.new('dir_1/dir_1_1', [wiki.find_page('dir_1/dir_1_1/page_3')]) + end + let(:dir_2) do + pages = [wiki.find_page('dir_2/page_5'), + wiki.find_page('dir_2/page_4')] + WikiDirectory.new('dir_2', pages) + end + + it 'returns an array with pages and directories' do + expected_grouped_entries = [page_1, dir_1, dir_1_1, dir_2] + + grouped_entries = WikiPage.group_by_directory(wiki.pages) + + grouped_entries.each_with_index do |page_or_dir, i| + expected_page_or_dir = expected_grouped_entries[i] + expected_slugs = get_slugs(expected_page_or_dir) + slugs = get_slugs(page_or_dir) + + expect(slugs).to match_array(expected_slugs) + end + end + + it 'returns an array sorted by alphabetical position' do + # Directories and pages within directories are sorted alphabetically. + # Pages at root come before everything. + expected_order = ['page_1', 'dir_1/page_2', 'dir_1/dir_1_1/page_3', + 'dir_2/page_4', 'dir_2/page_5'] + + grouped_entries = WikiPage.group_by_directory(wiki.pages) + + actual_order = + grouped_entries.map do |page_or_dir| + get_slugs(page_or_dir) + end. + flatten + expect(actual_order).to eq(expected_order) + end + end + end + + describe '.unhyphenize' do + it 'removes hyphens from a name' do + name = 'a-name--with-hyphens' + + expect(WikiPage.unhyphenize(name)).to eq('a name with hyphens') + end + end + describe "#initialize" do context "when initialized with an existing gollum page" do before do @@ -189,6 +258,26 @@ describe WikiPage, models: true do end end + describe '#directory' do + context 'when the page is at the root directory' do + it 'returns an empty string' do + create_page('file', 'content') + page = wiki.find_page('file') + + expect(page.directory).to eq('') + end + end + + context 'when the page is inside an actual directory' do + it 'returns the full directory hierarchy' do + create_page('dir_1/dir_1_1/file', 'content') + page = wiki.find_page('dir_1/dir_1_1/file') + + expect(page.directory).to eq('dir_1/dir_1_1') + end + end + end + describe '#historical?' do before do create_page('Update', 'content') @@ -221,6 +310,27 @@ describe WikiPage, models: true do end end + describe '#to_partial_path' do + it 'returns the relative path to the partial to be used' do + page = build(:wiki_page) + + expect(page.to_partial_path).to eq('projects/wikis/wiki_page') + end + end + + describe '#==' do + let(:original_wiki_page) { create(:wiki_page) } + + it 'returns true for identical wiki page' do + expect(original_wiki_page).to eq(original_wiki_page) + end + + it 'returns false for updated wiki page' do + updated_wiki_page = original_wiki_page.update("Updated content") + expect(original_wiki_page).not_to eq(updated_wiki_page) + end + end + private def remove_temp_repo(path) @@ -239,4 +349,12 @@ describe WikiPage, models: true do page = wiki.wiki.paged(title) wiki.wiki.delete_page(page, commit_details) end + + def get_slugs(page_or_dir) + if page_or_dir.is_a? WikiPage + [page_or_dir.slug] + else + page_or_dir.pages.present? ? page_or_dir.pages.map(&:slug) : [] + end + end end diff --git a/spec/policies/user_policy_spec.rb b/spec/policies/user_policy_spec.rb new file mode 100644 index 00000000000..d5761390d39 --- /dev/null +++ b/spec/policies/user_policy_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe UserPolicy, models: true do + let(:current_user) { create(:user) } + let(:user) { create(:user) } + + subject { described_class.abilities(current_user, user).to_set } + + describe "reading a user's information" do + it { is_expected.to include(:read_user) } + end + + describe "destroying a user" do + context "when a regular user tries to destroy another regular user" do + it { is_expected.not_to include(:destroy_user) } + end + + context "when a regular user tries to destroy themselves" do + let(:current_user) { user } + + it { is_expected.to include(:destroy_user) } + end + + context "when an admin user tries to destroy a regular user" do + let(:current_user) { create(:user, :admin) } + + it { is_expected.to include(:destroy_user) } + end + + context "when an admin user tries to destroy a ghost user" do + let(:current_user) { create(:user, :admin) } + let(:user) { create(:user, :ghost) } + + it { is_expected.not_to include(:destroy_user) } + end + end +end diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb index e487297748b..46edbd49b28 100644 --- a/spec/requests/api/access_requests_spec.rb +++ b/spec/requests/api/access_requests_spec.rb @@ -48,6 +48,7 @@ describe API::AccessRequests, api: true do get api("/#{source_type.pluralize}/#{source.id}/access_requests", master) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(1) end @@ -199,7 +200,7 @@ describe API::AccessRequests, api: true do expect do delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", access_requester) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) end.to change { source.requesters.count }.by(-1) end end @@ -209,7 +210,7 @@ describe API::AccessRequests, api: true do expect do delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", master) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) end.to change { source.requesters.count }.by(-1) end diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb index c8e8f31cc1f..9756991162e 100644 --- a/spec/requests/api/award_emoji_spec.rb +++ b/spec/requests/api/award_emoji_spec.rb @@ -34,6 +34,7 @@ describe API::AwardEmoji, api: true do get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['name']).to eq(downvote.name) end @@ -241,9 +242,9 @@ describe API::AwardEmoji, api: true do it 'deletes the award' do expect do delete api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/#{award_emoji.id}", user) - end.to change { issue.award_emoji.count }.from(1).to(0) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) + end.to change { issue.award_emoji.count }.from(1).to(0) end it 'returns a 404 error when the award emoji can not be found' do @@ -257,9 +258,9 @@ describe API::AwardEmoji, api: true do it 'deletes the award' do expect do delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user) - end.to change { merge_request.award_emoji.count }.from(1).to(0) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) + end.to change { merge_request.award_emoji.count }.from(1).to(0) end it 'returns a 404 error when note id not found' do @@ -276,9 +277,9 @@ describe API::AwardEmoji, api: true do it 'deletes the award' do expect do delete api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user) - end.to change { snippet.award_emoji.count }.from(1).to(0) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) + end.to change { snippet.award_emoji.count }.from(1).to(0) end end end @@ -289,9 +290,9 @@ describe API::AwardEmoji, api: true do it 'deletes the award' do expect do delete api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji/#{rocket.id}", user) - end.to change { note.award_emoji.count }.from(1).to(0) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) + end.to change { note.award_emoji.count }.from(1).to(0) end end end diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb index c14c3cb1ce7..87c36639cd4 100644 --- a/spec/requests/api/boards_spec.rb +++ b/spec/requests/api/boards_spec.rb @@ -55,6 +55,7 @@ describe API::Boards, api: true do get api(base_url, user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(board.id) @@ -72,6 +73,7 @@ describe API::Boards, api: true do get api(base_url, user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) expect(json_response.first['label']['name']).to eq(dev_label.title) @@ -193,8 +195,7 @@ describe API::Boards, api: true do it "deletes the list if an admin requests it" do delete api("#{base_url}/#{dev_list.id}", owner) - expect(response).to have_http_status(200) - expect(json_response['position']).to eq(1) + expect(response).to have_http_status(204) end end end diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index 5a3ffc284f2..ab5a7e4d3de 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -17,8 +17,10 @@ describe API::Branches, api: true do it "returns an array of project branches" do project.repository.expire_all_method_caches - get api("/projects/#{project.id}/repository/branches", user) + get api("/projects/#{project.id}/repository/branches", user), per_page: 100 + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array branch_names = json_response.map { |x| x['name'] } expect(branch_names).to match_array(project.repository.branch_names) @@ -31,7 +33,18 @@ describe API::Branches, api: true do expect(response).to have_http_status(200) expect(json_response['name']).to eq(branch_name) - expect(json_response['commit']['id']).to eq(branch_sha) + json_commit = json_response['commit'] + expect(json_commit['id']).to eq(branch_sha) + expect(json_commit).to have_key('short_id') + expect(json_commit).to have_key('title') + expect(json_commit).to have_key('message') + expect(json_commit).to have_key('author_name') + expect(json_commit).to have_key('author_email') + expect(json_commit).to have_key('authored_date') + expect(json_commit).to have_key('committer_name') + expect(json_commit).to have_key('committer_email') + expect(json_commit).to have_key('committed_date') + expect(json_commit).to have_key('parent_ids') expect(json_response['merged']).to eq(false) expect(json_response['protected']).to eq(false) expect(json_response['developers_can_push']).to eq(false) @@ -259,7 +272,7 @@ describe API::Branches, api: true do describe "POST /projects/:id/repository/branches" do it "creates a new branch" do post api("/projects/#{project.id}/repository/branches", user), - branch_name: 'feature1', + branch: 'feature1', ref: branch_sha expect(response).to have_http_status(201) @@ -270,14 +283,14 @@ describe API::Branches, api: true do it "denies for user without push access" do post api("/projects/#{project.id}/repository/branches", user2), - branch_name: branch_name, + branch: branch_name, ref: branch_sha expect(response).to have_http_status(403) end it 'returns 400 if branch name is invalid' do post api("/projects/#{project.id}/repository/branches", user), - branch_name: 'new design', + branch: 'new design', ref: branch_sha expect(response).to have_http_status(400) expect(json_response['message']).to eq('Branch name is invalid') @@ -285,12 +298,12 @@ describe API::Branches, api: true do it 'returns 400 if branch already exists' do post api("/projects/#{project.id}/repository/branches", user), - branch_name: 'new_design1', + branch: 'new_design1', ref: branch_sha expect(response).to have_http_status(201) post api("/projects/#{project.id}/repository/branches", user), - branch_name: 'new_design1', + branch: 'new_design1', ref: branch_sha expect(response).to have_http_status(400) expect(json_response['message']).to eq('Branch already exists') @@ -298,7 +311,7 @@ describe API::Branches, api: true do it 'returns 400 if ref name is invalid' do post api("/projects/#{project.id}/repository/branches", user), - branch_name: 'new_design3', + branch: 'new_design3', ref: 'foo' expect(response).to have_http_status(400) expect(json_response['message']).to eq('Invalid reference name') @@ -312,15 +325,14 @@ describe API::Branches, api: true do it "removes branch" do delete api("/projects/#{project.id}/repository/branches/#{branch_name}", user) - expect(response).to have_http_status(200) - expect(json_response['branch_name']).to eq(branch_name) + + expect(response).to have_http_status(204) end it "removes a branch with dots in the branch name" do delete api("/projects/#{project.id}/repository/branches/with.1.2.3", user) - expect(response).to have_http_status(200) - expect(json_response['branch_name']).to eq("with.1.2.3") + expect(response).to have_http_status(204) end it 'returns 404 if branch not exists' do @@ -347,9 +359,11 @@ describe API::Branches, api: true do allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true) end - it 'returns 200' do + it 'returns 202 with json body' do delete api("/projects/#{project.id}/repository/merged_branches", user) - expect(response).to have_http_status(200) + + expect(response).to have_http_status(202) + expect(json_response['message']).to eql('202 Accepted') end it 'returns a 403 error if guest' do diff --git a/spec/requests/api/broadcast_messages_spec.rb b/spec/requests/api/broadcast_messages_spec.rb index 7c9078b2864..024fa66848c 100644 --- a/spec/requests/api/broadcast_messages_spec.rb +++ b/spec/requests/api/broadcast_messages_spec.rb @@ -25,6 +25,7 @@ describe API::BroadcastMessages, api: true do get api('/broadcast_messages', admin) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers 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)) @@ -173,8 +174,11 @@ describe API::BroadcastMessages, api: true do end it 'deletes the broadcast message for admins' do - expect { delete api("/broadcast_messages/#{message.id}", admin) } - .to change { BroadcastMessage.count }.by(-1) + expect do + delete api("/broadcast_messages/#{message.id}", admin) + + expect(response).to have_http_status(204) + end.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 834c4e52693..76a10a2374c 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -16,12 +16,15 @@ describe API::Builds, api: true do let(:query) { '' } before do + create(:ci_build, :skipped, pipeline: pipeline) + get api("/projects/#{project.id}/builds?#{query}", api_user) end context 'authorized user' do it 'returns project builds' do expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array end @@ -48,6 +51,18 @@ describe API::Builds, api: true do end end + context 'filter project with scope skipped' do + let(:query) { 'scope=skipped' } + let(:json_build) { json_response.first } + + it 'return builds with status skipped' do + expect(response).to have_http_status 200 + expect(json_response).to be_an Array + expect(json_response.length).to eq 1 + expect(json_build['status']).to eq 'skipped' + end + end + context 'filter project with array of scope elements' do let(:query) { 'scope[0]=pending&scope[1]=running' } @@ -97,6 +112,7 @@ describe API::Builds, api: true do it 'returns project jobs for specific commit' do expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq 2 end diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb index 88361def3cf..d8b3cc041a5 100644 --- a/spec/requests/api/commit_statuses_spec.rb +++ b/spec/requests/api/commit_statuses_spec.rb @@ -5,12 +5,15 @@ describe API::CommitStatuses, api: true do let!(:project) { create(:project, :repository) } let(:commit) { project.repository.commit } - let(:commit_status) { create(:commit_status, pipeline: pipeline) } let(:guest) { create_user(:guest) } let(:reporter) { create_user(:reporter) } let(:developer) { create_user(:developer) } let(:sha) { commit.id } + let(:commit_status) do + create(:commit_status, status: :pending, pipeline: pipeline) + end + describe "GET /projects/:id/repository/commits/:sha/statuses" do let(:get_url) { "/projects/#{project.id}/repository/commits/#{sha}/statuses" } @@ -18,10 +21,6 @@ describe API::CommitStatuses, api: true do let!(:master) { project.pipelines.create(sha: commit.id, ref: 'master') } let!(:develop) { project.pipelines.create(sha: commit.id, ref: 'develop') } - it_behaves_like 'a paginated resources' do - let(:request) { get api(get_url, reporter) } - end - context "reporter user" do let(:statuses_id) { json_response.map { |status| status['id'] } } @@ -42,6 +41,7 @@ describe API::CommitStatuses, api: true do it 'returns latest commit statuses' do expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(statuses_id).to contain_exactly(status3.id, status4.id, status5.id, status6.id) json_response.sort_by!{ |status| status['id'] } @@ -54,7 +54,7 @@ describe API::CommitStatuses, api: true do it 'returns all commit statuses' do expect(response).to have_http_status(200) - + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(statuses_id).to contain_exactly(status1.id, status2.id, status3.id, status4.id, @@ -67,7 +67,7 @@ describe API::CommitStatuses, api: true do it 'returns latest commit statuses for specific ref' do expect(response).to have_http_status(200) - + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(statuses_id).to contain_exactly(status3.id, status5.id) end @@ -78,7 +78,7 @@ describe API::CommitStatuses, api: true do it 'return latest commit statuses for specific name' do expect(response).to have_http_status(200) - + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(statuses_id).to contain_exactly(status4.id, status5.id) end @@ -151,24 +151,62 @@ describe API::CommitStatuses, api: true do end context 'with all optional parameters' do - before do - optional_params = { state: 'success', - context: 'coverage', - ref: 'develop', - description: 'test', - target_url: 'http://gitlab.com/status' } + context 'when creating a commit status' do + it 'creates commit status' do + post api(post_url, developer), { + state: 'success', + context: 'coverage', + ref: 'develop', + description: 'test', + coverage: 80.0, + target_url: 'http://gitlab.com/status' + } - post api(post_url, developer), optional_params + expect(response).to have_http_status(201) + expect(json_response['sha']).to eq(commit.id) + expect(json_response['status']).to eq('success') + expect(json_response['name']).to eq('coverage') + expect(json_response['ref']).to eq('develop') + expect(json_response['coverage']).to eq(80.0) + expect(json_response['description']).to eq('test') + expect(json_response['target_url']).to eq('http://gitlab.com/status') + end end - it 'creates commit status' do - expect(response).to have_http_status(201) - expect(json_response['sha']).to eq(commit.id) - expect(json_response['status']).to eq('success') - expect(json_response['name']).to eq('coverage') - expect(json_response['ref']).to eq('develop') - expect(json_response['description']).to eq('test') - expect(json_response['target_url']).to eq('http://gitlab.com/status') + context 'when updatig a commit status' do + before do + post api(post_url, developer), { + state: 'running', + context: 'coverage', + ref: 'develop', + description: 'coverage test', + coverage: 0.0, + target_url: 'http://gitlab.com/status' + } + + post api(post_url, developer), { + state: 'success', + name: 'coverage', + ref: 'develop', + description: 'new description', + coverage: 90.0 + } + end + + it 'updates a commit status' do + expect(response).to have_http_status(201) + expect(json_response['sha']).to eq(commit.id) + expect(json_response['status']).to eq('success') + expect(json_response['name']).to eq('coverage') + expect(json_response['ref']).to eq('develop') + expect(json_response['coverage']).to eq(90.0) + expect(json_response['description']).to eq('new description') + expect(json_response['target_url']).to eq('http://gitlab.com/status') + end + + it 'does not create a new commit status' do + expect(CommitStatus.count).to eq 1 + end end end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index af9028a8978..5190fcca2d1 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -72,7 +72,7 @@ describe API::Commits, api: true do get api("/projects/#{project.id}/repository/commits?since=invalid-date", user) expect(response).to have_http_status(400) - expect(json_response['message']).to include "\"since\" must be a timestamp in ISO 8601 format" + expect(json_response['error']).to eq('since is invalid') end end @@ -107,7 +107,7 @@ describe API::Commits, api: true do let(:message) { 'Created file' } let!(:invalid_c_params) do { - branch_name: 'master', + branch: 'master', commit_message: message, actions: [ { @@ -120,7 +120,7 @@ describe API::Commits, api: true do end let!(:valid_c_params) do { - branch_name: 'master', + branch: 'master', commit_message: message, actions: [ { @@ -148,7 +148,7 @@ describe API::Commits, api: true do end context 'with project path in URL' do - let(:url) { "/projects/#{project.namespace.path}%2F#{project.path}/repository/commits" } + let(:url) { "/projects/#{project.full_path.gsub('/', '%2F')}/repository/commits" } it 'a new file in project repo' do post api(url, user), valid_c_params @@ -162,7 +162,7 @@ describe API::Commits, api: true do let(:message) { 'Deleted file' } let!(:invalid_d_params) do { - branch_name: 'markdown', + branch: 'markdown', commit_message: message, actions: [ { @@ -174,7 +174,7 @@ describe API::Commits, api: true do end let!(:valid_d_params) do { - branch_name: 'markdown', + branch: 'markdown', commit_message: message, actions: [ { @@ -203,7 +203,7 @@ describe API::Commits, api: true do let(:message) { 'Moved file' } let!(:invalid_m_params) do { - branch_name: 'feature', + branch: 'feature', commit_message: message, actions: [ { @@ -217,7 +217,7 @@ describe API::Commits, api: true do end let!(:valid_m_params) do { - branch_name: 'feature', + branch: 'feature', commit_message: message, actions: [ { @@ -248,7 +248,7 @@ describe API::Commits, api: true do let(:message) { 'Updated file' } let!(:invalid_u_params) do { - branch_name: 'master', + branch: 'master', commit_message: message, actions: [ { @@ -261,7 +261,7 @@ describe API::Commits, api: true do end let!(:valid_u_params) do { - branch_name: 'master', + branch: 'master', commit_message: message, actions: [ { @@ -291,7 +291,7 @@ describe API::Commits, api: true do let(:message) { 'Multiple actions' } let!(:invalid_mo_params) do { - branch_name: 'master', + branch: 'master', commit_message: message, actions: [ { @@ -319,7 +319,7 @@ describe API::Commits, api: true do end let!(:valid_mo_params) do { - branch_name: 'master', + branch: 'master', commit_message: message, actions: [ { @@ -367,11 +367,21 @@ describe API::Commits, api: true do get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) expect(response).to have_http_status(200) - expect(json_response['id']).to eq(project.repository.commit.id) - expect(json_response['title']).to eq(project.repository.commit.title) - expect(json_response['stats']['additions']).to eq(project.repository.commit.stats.additions) - expect(json_response['stats']['deletions']).to eq(project.repository.commit.stats.deletions) - expect(json_response['stats']['total']).to eq(project.repository.commit.stats.total) + commit = project.repository.commit + expect(json_response['id']).to eq(commit.id) + expect(json_response['short_id']).to eq(commit.short_id) + expect(json_response['title']).to eq(commit.title) + expect(json_response['message']).to eq(commit.safe_message) + expect(json_response['author_name']).to eq(commit.author_name) + expect(json_response['author_email']).to eq(commit.author_email) + expect(json_response['authored_date']).to eq(commit.authored_date.iso8601(3)) + expect(json_response['committer_name']).to eq(commit.committer_name) + expect(json_response['committer_email']).to eq(commit.committer_email) + expect(json_response['committed_date']).to eq(commit.committed_date.iso8601(3)) + expect(json_response['parent_ids']).to eq(commit.parent_ids) + expect(json_response['stats']['additions']).to eq(commit.stats.additions) + expect(json_response['stats']['deletions']).to eq(commit.stats.deletions) + expect(json_response['stats']['total']).to eq(commit.stats.total) end it "returns a 404 error if not found" do @@ -446,6 +456,7 @@ describe API::Commits, api: true do it 'returns merge_request comments' do get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) expect(json_response.first['note']).to eq('a comment on a commit') @@ -464,6 +475,20 @@ describe API::Commits, api: true do expect(response).to have_http_status(401) end end + + context 'when the commit is present on two projects' do + let(:forked_project) { create(:project, :repository, creator: user2, namespace: user2.namespace) } + let!(:forked_project_note) { create(:note_on_commit, author: user2, project: forked_project, commit_id: forked_project.repository.commit.id, note: 'a comment on a commit for fork') } + + it 'returns the comments for the target project' do + get api("/projects/#{forked_project.id}/repository/commits/#{forked_project.repository.commit.id}/comments", user2) + + expect(response).to have_http_status(200) + expect(json_response.length).to eq(1) + expect(json_response.first['note']).to eq('a comment on a commit for fork') + expect(json_response.first['author']['id']).to eq(user2.id) + end + end end describe 'POST :id/repository/commits/:sha/cherry_pick' do diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb index 766234d7104..4f4b18cf0e0 100644 --- a/spec/requests/api/deploy_keys_spec.rb +++ b/spec/requests/api/deploy_keys_spec.rb @@ -35,6 +35,7 @@ describe API::DeployKeys, api: true do get api('/deploy_keys', admin) expect(response.status).to eq(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['id']).to eq(deploy_keys_project.deploy_key.id) end @@ -48,6 +49,7 @@ describe API::DeployKeys, api: true do get api("/projects/#{project.id}/deploy_keys", admin) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['title']).to eq(deploy_key.title) end @@ -114,6 +116,8 @@ describe API::DeployKeys, api: true do it 'should delete existing key' do expect do delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin) + + expect(response).to have_http_status(204) end.to change{ project.deploy_keys.count }.by(-1) end @@ -146,25 +150,4 @@ describe API::DeployKeys, api: true do end end end - - describe 'DELETE /projects/:id/deploy_keys/:key_id/disable' do - context 'when the user can admin the project' do - it 'disables the key' do - expect do - delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}/disable", admin) - end.to change { project.deploy_keys.count }.from(1).to(0) - - expect(response).to have_http_status(200) - expect(json_response['id']).to eq(deploy_key.id) - end - end - - context 'when authenticated as non-admin user' do - it 'should return a 404 error' do - delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}/disable", user) - - expect(response).to have_http_status(404) - end - end - end end diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb index 31e3cfa1b2f..e55575ffbda 100644 --- a/spec/requests/api/deployments_spec.rb +++ b/spec/requests/api/deployments_spec.rb @@ -14,14 +14,11 @@ describe API::Deployments, api: true do describe 'GET /projects/:id/deployments' do context 'as member of the project' do - it_behaves_like 'a paginated resources' do - let(:request) { get api("/projects/#{project.id}/deployments", user) } - end - it 'returns projects deployments' do get api("/projects/#{project.id}/deployments", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(1) expect(json_response.first['iid']).to eq(deployment.iid) diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb index 8168b613766..f2fd1dfc8db 100644 --- a/spec/requests/api/environments_spec.rb +++ b/spec/requests/api/environments_spec.rb @@ -14,19 +14,17 @@ describe API::Environments, api: true do describe 'GET /projects/:id/environments' do context 'as member of the project' do - it_behaves_like 'a paginated resources' do - let(:request) { get api("/projects/#{project.id}/environments", user) } - end - it 'returns project environments' do get api("/projects/#{project.id}/environments", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(1) expect(json_response.first['name']).to eq(environment.name) expect(json_response.first['external_url']).to eq(environment.external_url) expect(json_response.first['project']['id']).to eq(project.id) + expect(json_response.first['project']['visibility']).to be_present end end @@ -125,7 +123,7 @@ describe API::Environments, api: true do it 'returns a 200 for an existing environment' do delete api("/projects/#{project.id}/environments/#{environment.id}", user) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) end it 'returns a 404 for non existing id' do @@ -144,4 +142,39 @@ describe API::Environments, api: true do end end end + + describe 'POST /projects/:id/environments/:environment_id/stop' do + context 'as a master' do + context 'with a stoppable environment' do + before do + environment.update(state: :available) + + post api("/projects/#{project.id}/environments/#{environment.id}/stop", user) + end + + it 'returns a 200' do + expect(response).to have_http_status(200) + end + + it 'actually stops the environment' do + expect(environment.reload).to be_stopped + end + end + + it 'returns a 404 for non existing id' do + post api("/projects/#{project.id}/environments/12345/stop", user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Not found') + end + end + + context 'a non member' do + it 'rejects the request' do + post api("/projects/#{project.id}/environments/#{environment.id}/stop", non_member) + + expect(response).to have_http_status(404) + end + end + end end diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index 5e26e779366..91f8a35e045 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -104,7 +104,7 @@ describe API::Files, api: true do let(:valid_params) do { file_path: 'newfile.rb', - branch_name: 'master', + branch: 'master', content: 'puts 8', commit_message: 'Added newfile' } @@ -127,7 +127,7 @@ describe API::Files, api: true do end it "returns a 400 if editor fails to create file" do - allow_any_instance_of(Repository).to receive(:commit_file). + allow_any_instance_of(Repository).to receive(:create_file). and_return(false) post api("/projects/#{project.id}/repository/files", user), valid_params @@ -147,13 +147,27 @@ describe API::Files, api: true do expect(last_commit.author_name).to eq(author_name) end end + + context 'when the repo is empty' do + let!(:project) { create(:project_empty_repo, namespace: user.namespace ) } + + it "creates a new file in project repo" do + post api("/projects/#{project.id}/repository/files", user), valid_params + + expect(response).to have_http_status(201) + expect(json_response['file_path']).to eq('newfile.rb') + last_commit = project.repository.commit.raw + expect(last_commit.author_email).to eq(user.email) + expect(last_commit.author_name).to eq(user.name) + end + end end describe "PUT /projects/:id/repository/files" do let(:valid_params) do { file_path: file_path, - branch_name: 'master', + branch: 'master', content: 'puts 8', commit_message: 'Changed file' } @@ -193,7 +207,7 @@ describe API::Files, api: true do let(:valid_params) do { file_path: file_path, - branch_name: 'master', + branch: 'master', commit_message: 'Changed file' } end @@ -201,11 +215,7 @@ describe API::Files, api: true do it "deletes existing file in project repo" do delete api("/projects/#{project.id}/repository/files", user), valid_params - expect(response).to have_http_status(200) - expect(json_response['file_path']).to eq(file_path) - last_commit = project.repository.commit.raw - expect(last_commit.author_email).to eq(user.email) - expect(last_commit.author_name).to eq(user.name) + expect(response).to have_http_status(204) end it "returns a 400 bad request if no params given" do @@ -215,7 +225,7 @@ describe API::Files, api: true do end it "returns a 400 if fails to create file" do - allow_any_instance_of(Repository).to receive(:remove_file).and_return(false) + allow_any_instance_of(Repository).to receive(:delete_file).and_return(false) delete api("/projects/#{project.id}/repository/files", user), valid_params @@ -228,10 +238,7 @@ describe API::Files, api: true do delete api("/projects/#{project.id}/repository/files", user), valid_params - expect(response).to have_http_status(200) - last_commit = project.repository.commit.raw - expect(last_commit.author_email).to eq(author_email) - expect(last_commit.author_name).to eq(author_name) + expect(response).to have_http_status(204) end end end @@ -241,7 +248,7 @@ describe API::Files, api: true do let(:put_params) do { file_path: file_path, - branch_name: 'master', + branch: 'master', content: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=', commit_message: 'Binary file with a \n should not be touched', encoding: 'base64' diff --git a/spec/requests/api/fork_spec.rb b/spec/requests/api/fork_spec.rb deleted file mode 100644 index 92ac4fd334d..00000000000 --- a/spec/requests/api/fork_spec.rb +++ /dev/null @@ -1,134 +0,0 @@ -require 'spec_helper' - -describe API::Projects, api: true do - include ApiHelpers - let(:user) { create(:user) } - let(:user2) { create(:user) } - let(:admin) { create(:admin) } - let(:group) { create(:group) } - let(:group2) do - group = create(:group, name: 'group2_name') - group.add_owner(user2) - group - end - - describe 'POST /projects/fork/:id' do - let(:project) do - create(:project, :repository, creator: user, namespace: user.namespace) - end - - before do - project.add_reporter(user2) - end - - context 'when authenticated' do - it 'forks if user has sufficient access to project' do - post api("/projects/fork/#{project.id}", user2) - - expect(response).to have_http_status(201) - expect(json_response['name']).to eq(project.name) - expect(json_response['path']).to eq(project.path) - expect(json_response['owner']['id']).to eq(user2.id) - expect(json_response['namespace']['id']).to eq(user2.namespace.id) - expect(json_response['forked_from_project']['id']).to eq(project.id) - end - - it 'forks if user is admin' do - post api("/projects/fork/#{project.id}", admin) - - expect(response).to have_http_status(201) - expect(json_response['name']).to eq(project.name) - expect(json_response['path']).to eq(project.path) - expect(json_response['owner']['id']).to eq(admin.id) - expect(json_response['namespace']['id']).to eq(admin.namespace.id) - expect(json_response['forked_from_project']['id']).to eq(project.id) - end - - it 'fails on missing project access for the project to fork' do - new_user = create(:user) - post api("/projects/fork/#{project.id}", new_user) - - expect(response).to have_http_status(404) - expect(json_response['message']).to eq('404 Project Not Found') - end - - it 'fails if forked project exists in the user namespace' do - post api("/projects/fork/#{project.id}", user) - - expect(response).to have_http_status(409) - expect(json_response['message']['name']).to eq(['has already been taken']) - expect(json_response['message']['path']).to eq(['has already been taken']) - end - - it 'fails if project to fork from does not exist' do - post api('/projects/fork/424242', user) - - expect(response).to have_http_status(404) - expect(json_response['message']).to eq('404 Project Not Found') - end - - it 'forks with explicit own user namespace id' do - post api("/projects/fork/#{project.id}", user2), namespace: user2.namespace.id - - expect(response).to have_http_status(201) - expect(json_response['owner']['id']).to eq(user2.id) - end - - it 'forks with explicit own user name as namespace' do - post api("/projects/fork/#{project.id}", user2), namespace: user2.username - - expect(response).to have_http_status(201) - expect(json_response['owner']['id']).to eq(user2.id) - end - - it 'forks to another user when admin' do - post api("/projects/fork/#{project.id}", admin), namespace: user2.username - - expect(response).to have_http_status(201) - expect(json_response['owner']['id']).to eq(user2.id) - end - - it 'fails if trying to fork to another user when not admin' do - post api("/projects/fork/#{project.id}", user2), namespace: admin.namespace.id - - expect(response).to have_http_status(404) - end - - it 'fails if trying to fork to non-existent namespace' do - post api("/projects/fork/#{project.id}", user2), namespace: 42424242 - - expect(response).to have_http_status(404) - expect(json_response['message']).to eq('404 Target Namespace Not Found') - end - - it 'forks to owned group' do - post api("/projects/fork/#{project.id}", user2), namespace: group2.name - - expect(response).to have_http_status(201) - expect(json_response['namespace']['name']).to eq(group2.name) - end - - it 'fails to fork to not owned group' do - post api("/projects/fork/#{project.id}", user2), namespace: group.name - - expect(response).to have_http_status(404) - end - - it 'forks to not owned group when admin' do - post api("/projects/fork/#{project.id}", admin), namespace: group.name - - expect(response).to have_http_status(201) - expect(json_response['namespace']['name']).to eq(group.name) - end - end - - context 'when unauthenticated' do - it 'returns authentication error' do - post api("/projects/fork/#{project.id}") - - expect(response).to have_http_status(401) - expect(json_response['message']).to eq('401 Unauthorized') - end - end - end -end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index f78bde6f53a..2b8fd7e31a1 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -33,6 +33,7 @@ describe API::Groups, api: true do get api("/groups", user1) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response) @@ -43,6 +44,7 @@ describe API::Groups, api: true do get api("/groups", user1), statistics: true expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first).not_to include 'statistics' end @@ -53,6 +55,7 @@ describe API::Groups, api: true do get api("/groups", admin) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) end @@ -61,6 +64,7 @@ describe API::Groups, api: true do get api("/groups", admin) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first).not_to include('statistics') end @@ -78,6 +82,7 @@ describe API::Groups, api: true do get api("/groups", admin), statistics: true expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response) .to satisfy_one { |group| group['statistics'] == attributes } @@ -89,6 +94,7 @@ describe API::Groups, api: true do get api("/groups", admin), skip_groups: [group2.id] expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) end @@ -103,6 +109,7 @@ describe API::Groups, api: true do get api("/groups", user1), all_available: true expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_groups).to contain_exactly(public_group.name, group1.name) end @@ -120,6 +127,7 @@ describe API::Groups, api: true do get api("/groups", user1) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_groups).to eq([group3.name, group1.name]) end @@ -128,6 +136,7 @@ describe API::Groups, api: true do get api("/groups", user1), sort: "desc" expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_groups).to eq([group1.name, group3.name]) end @@ -136,26 +145,18 @@ describe API::Groups, api: true do get api("/groups", user1), order_by: "path" expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_groups).to eq([group1.name, group3.name]) end end - end - - describe 'GET /groups/owned' do - context 'when unauthenticated' do - it 'returns authentication error' do - get api('/groups/owned') - - expect(response).to have_http_status(401) - end - end - context 'when authenticated as group owner' do + context 'when using owned in the request' do it 'returns an array of groups the user owns' do - get api('/groups/owned', user2) + get api('/groups', user2), owned: true expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['name']).to eq(group2.name) end @@ -175,7 +176,7 @@ describe API::Groups, api: true do expect(json_response['name']).to eq(group1.name) expect(json_response['path']).to eq(group1.path) expect(json_response['description']).to eq(group1.description) - expect(json_response['visibility_level']).to eq(group1.visibility_level) + expect(json_response['visibility']).to eq(Gitlab::VisibilityLevel.string_level(group1.visibility_level)) expect(json_response['avatar_url']).to eq(group1.avatar_url) expect(json_response['web_url']).to eq(group1.web_url) expect(json_response['request_access_enabled']).to eq(group1.request_access_enabled) @@ -290,20 +291,22 @@ describe API::Groups, api: true do get api("/groups/#{group1.id}/projects", user1) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response.length).to eq(2) - project_names = json_response.map { |proj| proj['name' ] } + project_names = json_response.map { |proj| proj['name'] } expect(project_names).to match_array([project1.name, project3.name]) - expect(json_response.first['visibility_level']).to be_present + expect(json_response.first['visibility']).to be_present end it "returns the group's projects with simple representation" do get api("/groups/#{group1.id}/projects", user1), simple: true expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response.length).to eq(2) - project_names = json_response.map { |proj| proj['name' ] } + project_names = json_response.map { |proj| proj['name'] } expect(project_names).to match_array([project1.name, project3.name]) - expect(json_response.first['visibility_level']).not_to be_present + expect(json_response.first['visibility']).not_to be_present end it 'filters the groups projects' do @@ -312,6 +315,7 @@ describe API::Groups, api: true do get api("/groups/#{group1.id}/projects", user1), visibility: 'public' expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an(Array) expect(json_response.length).to eq(1) expect(json_response.first['name']).to eq(public_project.name) @@ -335,9 +339,30 @@ describe API::Groups, api: true do get api("/groups/#{group1.id}/projects", user3) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response.length).to eq(1) expect(json_response.first['name']).to eq(project3.name) end + + it 'only returns the projects owned by user' do + project2.group.add_owner(user3) + + get api("/groups/#{project2.group.id}/projects", user3), owned: true + + expect(response).to have_http_status(200) + expect(json_response.length).to eq(1) + expect(json_response.first['name']).to eq(project2.name) + end + + it 'only returns the projects starred by user' do + user1.starred_projects = [project1] + + get api("/groups/#{group1.id}/projects", user1), starred: true + + expect(response).to have_http_status(200) + expect(json_response.length).to eq(1) + expect(json_response.first['name']).to eq(project1.name) + end end context "when authenticated as admin" do @@ -345,6 +370,7 @@ describe API::Groups, api: true do get api("/groups/#{group2.id}/projects", admin) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response.length).to eq(1) expect(json_response.first['name']).to eq(project2.name) end @@ -361,7 +387,8 @@ describe API::Groups, api: true do get api("/groups/#{group1.path}/projects", admin) expect(response).to have_http_status(200) - project_names = json_response.map { |proj| proj['name' ] } + expect(response).to include_pagination_headers + project_names = json_response.map { |proj| proj['name'] } expect(project_names).to match_array([project1.name, project3.name]) end @@ -440,7 +467,7 @@ describe API::Groups, api: true do it "removes group" do delete api("/groups/#{group1.id}", user1) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) end it "does not remove a group if not an owner" do @@ -469,7 +496,7 @@ describe API::Groups, api: true do it "removes any existing group" do delete api("/groups/#{group2.id}", admin) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) end it "does not remove a non existing group" do @@ -482,7 +509,7 @@ describe API::Groups, api: true do describe "POST /groups/:id/projects/:project_id" do let(:project) { create(:empty_project) } - let(:project_path) { "#{project.namespace.path}%2F#{project.path}" } + let(:project_path) { project.full_path.gsub('/', '%2F') } before(:each) do allow_any_instance_of(Projects::TransferService). diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index ffeacb15f17..f18b8e98707 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -409,6 +409,34 @@ describe API::Internal, api: true do end end + describe 'POST /notify_post_receive' do + let(:valid_params) do + { repo_path: project.repository.path, secret_token: secret_token } + end + + before do + allow(Gitlab.config.gitaly).to receive(:socket_path).and_return('path/to/gitaly.socket') + end + + it "calls the Gitaly client if it's enabled" do + expect_any_instance_of(Gitlab::GitalyClient::Notifications). + to receive(:post_receive).with(project.repository.path) + + post api("/internal/notify_post_receive"), valid_params + + expect(response).to have_http_status(200) + end + + it "returns 500 if the gitaly call fails" do + expect_any_instance_of(Gitlab::GitalyClient::Notifications). + to receive(:post_receive).with(project.repository.path).and_raise(GRPC::Unavailable) + + post api("/internal/notify_post_receive"), valid_params + + expect(response).to have_http_status(500) + end + end + def project_with_repo_path(path) double().tap do |fake_project| allow(fake_project).to receive_message_chain('repository.path_to_repo' => path) diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index cca00df9591..710e4320fd1 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -68,7 +68,9 @@ describe API::Issues, api: true do context "when authenticated" do it "returns an array of issues" do get api("/issues", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['title']).to eq(issue.title) expect(json_response.last).to have_key('web_url') @@ -76,7 +78,9 @@ describe API::Issues, api: true do it 'returns an array of closed issues' do get api('/issues?state=closed', user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(closed_issue.id) @@ -84,7 +88,9 @@ describe API::Issues, api: true do it 'returns an array of opened issues' do get api('/issues?state=opened', user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(issue.id) @@ -92,7 +98,9 @@ describe API::Issues, api: true do it 'returns an array of all issues' do get api('/issues?state=all', user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) expect(json_response.first['id']).to eq(issue.id) @@ -101,31 +109,44 @@ describe API::Issues, api: true do it 'returns an array of labeled issues' do get api("/issues?labels=#{label.title}", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['labels']).to eq([label.title]) end - it 'returns an array of labeled issues when at least one label matches' do - get api("/issues?labels=#{label.title},foo,bar", user) + it 'returns an array of labeled issues when all labels matches' do + label_b = create(:label, title: 'foo', project: project) + label_c = create(:label, title: 'bar', project: project) + + create(:label_link, label: label_b, target: issue) + create(:label_link, label: label_c, target: issue) + + get api("/issues", user), labels: "#{label.title},#{label_b.title},#{label_c.title}" expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) - expect(json_response.first['labels']).to eq([label.title]) + expect(json_response.first['labels']).to eq([label_c.title, label_b.title, label.title]) end it 'returns an empty array if no issue matches labels' do get api('/issues?labels=foo,bar', user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) end it 'returns an array of labeled issues matching given state' do get api("/issues?labels=#{label.title}&state=opened", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['labels']).to eq([label.title]) @@ -134,7 +155,9 @@ describe API::Issues, api: true do it 'returns an empty array if no issue matches labels and state filters' do get api("/issues?labels=#{label.title}&state=closed", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) end @@ -143,6 +166,7 @@ describe API::Issues, api: true do get api("/issues?milestone=#{empty_milestone.title}", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) end @@ -151,6 +175,7 @@ describe API::Issues, api: true do get api("/issues?milestone=foo", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) end @@ -159,6 +184,7 @@ describe API::Issues, api: true do get api("/issues?milestone=#{milestone.title}", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) expect(json_response.first['id']).to eq(issue.id) @@ -170,6 +196,7 @@ describe API::Issues, api: true do '&state=closed', user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(closed_issue.id) @@ -179,43 +206,67 @@ describe API::Issues, api: true do get api("/issues?milestone=#{no_milestone_title}", author) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(confidential_issue.id) end + it 'returns an array of issues found by iids' do + get api('/issues', user), iids: [closed_issue.iid] + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(closed_issue.id) + end + + it 'returns an empty array if iid does not exist' do + get api("/issues", user), iids: [99999] + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + 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'] } + response_dates = json_response.map { |issue| issue['created_at'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers 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'] } + response_dates = json_response.map { |issue| issue['created_at'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers 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'] } + response_dates = json_response.map { |issue| issue['updated_at'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers 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'] } + response_dates = json_response.map { |issue| issue['updated_at'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_dates).to eq(response_dates.sort) end @@ -269,6 +320,7 @@ describe API::Issues, api: true do get api(base_url, non_member) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['title']).to eq(group_issue.title) @@ -278,6 +330,7 @@ describe API::Issues, api: true do get api(base_url, author) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) end @@ -286,6 +339,7 @@ describe API::Issues, api: true do get api(base_url, assignee) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) end @@ -294,6 +348,7 @@ describe API::Issues, api: true do get api(base_url, user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) end @@ -302,6 +357,7 @@ describe API::Issues, api: true do get api(base_url, admin) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) end @@ -310,6 +366,7 @@ describe API::Issues, api: true do get api("#{base_url}?labels=#{group_label.title}", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['labels']).to eq([group_label.title]) @@ -319,6 +376,41 @@ describe API::Issues, api: true do get api("#{base_url}?labels=#{group_label.title},foo,bar", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + + it 'returns an array of labeled issues when all labels matches' do + label_b = create(:label, title: 'foo', project: group_project) + label_c = create(:label, title: 'bar', project: group_project) + + create(:label_link, label: label_b, target: group_issue) + create(:label_link, label: label_c, target: group_issue) + + get api("#{base_url}", user), labels: "#{group_label.title},#{label_b.title},#{label_c.title}" + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['labels']).to eq([label_c.title, label_b.title, group_label.title]) + end + + it 'returns an array of issues found by iids' do + get api(base_url, user), iids: [group_issue.iid] + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(group_issue.id) + end + + it 'returns an empty array if iid does not exist' do + get api(base_url, user), iids: [99999] + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) end @@ -327,6 +419,7 @@ describe API::Issues, api: true do get api("#{base_url}?labels=foo,bar", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) end @@ -335,6 +428,7 @@ describe API::Issues, api: true do get api("#{base_url}?milestone=#{group_empty_milestone.title}", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) end @@ -343,6 +437,7 @@ describe API::Issues, api: true do get api("#{base_url}?milestone=foo", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) end @@ -351,6 +446,7 @@ describe API::Issues, api: true do get api("#{base_url}?milestone=#{group_milestone.title}", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(group_issue.id) @@ -361,6 +457,7 @@ describe API::Issues, api: true do '&state=closed', user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(group_closed_issue.id) @@ -370,6 +467,7 @@ describe API::Issues, api: true do get api("#{base_url}?milestone=#{no_milestone_title}", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(group_confidential_issue.id) @@ -377,36 +475,40 @@ describe API::Issues, api: true do it 'sorts by created_at descending by default' do get api(base_url, user) - response_dates = json_response.map { |issue| issue['created_at'] } + response_dates = json_response.map { |issue| issue['created_at'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers 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'] } + response_dates = json_response.map { |issue| issue['created_at'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers 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'] } + response_dates = json_response.map { |issue| issue['updated_at'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers 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'] } + response_dates = json_response.map { |issue| issue['updated_at'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_dates).to eq(response_dates.sort) end @@ -430,12 +532,17 @@ describe API::Issues, api: true do get api("/projects/#{restricted_project.id}/issues", non_member) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array expect(json_response).to eq([]) end it 'returns project issues without confidential issues for non project members' do get api("#{base_url}/issues", non_member) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) expect(json_response.first['title']).to eq(issue.title) @@ -443,7 +550,9 @@ describe API::Issues, api: true do it 'returns project issues without confidential issues for project members with guest role' do get api("#{base_url}/issues", guest) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) expect(json_response.first['title']).to eq(issue.title) @@ -451,7 +560,9 @@ describe API::Issues, api: true do it 'returns project confidential issues for author' do get api("#{base_url}/issues", author) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) expect(json_response.first['title']).to eq(issue.title) @@ -459,7 +570,9 @@ describe API::Issues, api: true do it 'returns project confidential issues for assignee' do get api("#{base_url}/issues", assignee) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) expect(json_response.first['title']).to eq(issue.title) @@ -467,7 +580,9 @@ describe API::Issues, api: true do it 'returns project issues with confidential issues for project members' do get api("#{base_url}/issues", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) expect(json_response.first['title']).to eq(issue.title) @@ -475,7 +590,9 @@ describe API::Issues, api: true do it 'returns project confidential issues for admin' do get api("#{base_url}/issues", admin) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) expect(json_response.first['title']).to eq(issue.title) @@ -483,38 +600,80 @@ describe API::Issues, api: true do it 'returns an array of labeled project issues' do get api("#{base_url}/issues?labels=#{label.title}", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['labels']).to eq([label.title]) end - it 'returns an array of labeled project issues where all labels match' do - get api("#{base_url}/issues?labels=#{label.title},foo,bar", user) + it 'returns an array of labeled issues when all labels matches' do + label_b = create(:label, title: 'foo', project: project) + label_c = create(:label, title: 'bar', project: project) + + create(:label_link, label: label_b, target: issue) + create(:label_link, label: label_c, target: issue) + + get api("#{base_url}/issues", user), labels: "#{label.title},#{label_b.title},#{label_c.title}" expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) - expect(json_response.first['labels']).to eq([label.title]) + expect(json_response.first['labels']).to eq([label_c.title, label_b.title, label.title]) + end + + it 'returns an array of issues found by iids' do + get api("#{base_url}/issues", user), iids: [issue.iid] + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(issue.id) + end + + it 'returns an empty array if iid does not exist' do + get api("#{base_url}/issues", user), iids: [99999] + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + + it 'returns an empty array if not all labels matches' do + get api("#{base_url}/issues?labels=#{label.title},foo", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) end it 'returns an empty array if no project issue matches labels' do get api("#{base_url}/issues?labels=foo,bar", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) end it 'returns an empty array if no issue matches milestone' do get api("#{base_url}/issues?milestone=#{empty_milestone.title}", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) end it 'returns an empty array if milestone does not exist' do get api("#{base_url}/issues?milestone=foo", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) end @@ -523,6 +682,7 @@ describe API::Issues, api: true do get api("#{base_url}/issues?milestone=#{milestone.title}", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) expect(json_response.first['id']).to eq(issue.id) @@ -530,9 +690,10 @@ describe API::Issues, api: true do end it 'returns an array of issues matching state in milestone' do - get api("#{base_url}/issues?milestone=#{milestone.title}"\ - '&state=closed', user) + get api("#{base_url}/issues?milestone=#{milestone.title}&state=closed", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(closed_issue.id) @@ -542,6 +703,7 @@ describe API::Issues, api: true do get api("#{base_url}/issues?milestone=#{no_milestone_title}", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(confidential_issue.id) @@ -549,36 +711,40 @@ describe API::Issues, api: true do it 'sorts by created_at descending by default' do get api("#{base_url}/issues", user) - response_dates = json_response.map { |issue| issue['created_at'] } + response_dates = json_response.map { |issue| issue['created_at'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers 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'] } + response_dates = json_response.map { |issue| issue['created_at'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers 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'] } + response_dates = json_response.map { |issue| issue['updated_at'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers 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'] } + response_dates = json_response.map { |issue| issue['updated_at'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_dates).to eq(response_dates.sort) end @@ -666,7 +832,7 @@ describe API::Issues, api: true do 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['labels']).to eq(%w(label label2)) expect(json_response['confidential']).to be_falsy end @@ -919,6 +1085,33 @@ describe API::Issues, api: true do end end + describe 'PUT /projects/:id/issues/:issue_id with spam filtering' do + let(:params) do + { + title: 'updated title', + description: 'content here', + labels: 'label, label2' + } + end + + it "does not create a new project issue" do + allow_any_instance_of(SpamService).to receive_messages(check_for_spam?: true) + allow_any_instance_of(AkismetService).to receive_messages(is_spam?: true) + + put api("/projects/#{project.id}/issues/#{issue.id}", user), params + + expect(response).to have_http_status(400) + expect(json_response['message']).to eq({ "error" => "Spam detected" }) + + spam_logs = SpamLog.all + expect(spam_logs.count).to eq(1) + expect(spam_logs[0].title).to eq('updated title') + expect(spam_logs[0].description).to eq('content here') + expect(spam_logs[0].user).to eq(user) + expect(spam_logs[0].noteable_type).to eq('Issue') + end + end + describe 'PUT /projects/:id/issues/:issue_id to update labels' do let!(:label) { create(:label, title: 'dummy', project: project) } let!(:label_link) { create(:label_link, label: label, target: issue) } @@ -1039,8 +1232,8 @@ describe API::Issues, api: true do it "deletes the issue if an admin requests it" do delete api("/projects/#{project.id}/issues/#{issue.id}", owner) - expect(response).to have_http_status(200) - expect(json_response['state']).to eq 'opened' + + expect(response).to have_http_status(204) end end @@ -1123,55 +1316,55 @@ describe API::Issues, api: true do end end - describe 'POST :id/issues/:issue_id/subscription' do + describe 'POST :id/issues/:issue_id/subscribe' do it 'subscribes to an issue' do - post api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2) + post api("/projects/#{project.id}/issues/#{issue.id}/subscribe", user2) expect(response).to have_http_status(201) expect(json_response['subscribed']).to eq(true) end it 'returns 304 if already subscribed' do - post api("/projects/#{project.id}/issues/#{issue.id}/subscription", user) + post api("/projects/#{project.id}/issues/#{issue.id}/subscribe", user) expect(response).to have_http_status(304) end it 'returns 404 if the issue is not found' do - post api("/projects/#{project.id}/issues/123/subscription", user) + post api("/projects/#{project.id}/issues/123/subscribe", user) expect(response).to have_http_status(404) end it 'returns 404 if the issue is confidential' do - post api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscription", non_member) + post api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscribe", non_member) expect(response).to have_http_status(404) end end - describe 'DELETE :id/issues/:issue_id/subscription' do + describe 'POST :id/issues/:issue_id/unsubscribe' do it 'unsubscribes from an issue' do - delete api("/projects/#{project.id}/issues/#{issue.id}/subscription", user) + post api("/projects/#{project.id}/issues/#{issue.id}/unsubscribe", user) - expect(response).to have_http_status(200) + expect(response).to have_http_status(201) expect(json_response['subscribed']).to eq(false) end it 'returns 304 if not subscribed' do - delete api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2) + post api("/projects/#{project.id}/issues/#{issue.id}/unsubscribe", user2) expect(response).to have_http_status(304) end it 'returns 404 if the issue is not found' do - delete api("/projects/#{project.id}/issues/123/subscription", user) + post api("/projects/#{project.id}/issues/123/unsubscribe", user) expect(response).to have_http_status(404) end it 'returns 404 if the issue is confidential' do - delete api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscription", non_member) + post api("/projects/#{project.id}/issues/#{confidential_issue.id}/unsubscribe", non_member) expect(response).to have_http_status(404) end diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb index a8cd787f398..a1adaba7b98 100644 --- a/spec/requests/api/labels_spec.rb +++ b/spec/requests/api/labels_spec.rb @@ -21,15 +21,16 @@ describe API::Labels, api: true do create(:labeled_issue, project: project, labels: [label1], author: user, state: :closed) create(:labeled_merge_request, labels: [priority_label], author: user, source_project: project ) - expected_keys = [ - 'id', 'name', 'color', 'description', - 'open_issues_count', 'closed_issues_count', 'open_merge_requests_count', - 'subscribed', 'priority' - ] + expected_keys = %w( + id name color description + open_issues_count closed_issues_count open_merge_requests_count + subscribed priority + ) get api("/projects/#{project.id}/labels", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(3) expect(json_response.first.keys).to match_array expected_keys @@ -174,9 +175,10 @@ describe API::Labels, api: true do end describe 'DELETE /projects/:id/labels' do - it 'returns 200 for existing label' do + it 'returns 204 for existing label' do delete api("/projects/#{project.id}/labels", user), name: 'label1' - expect(response).to have_http_status(200) + + expect(response).to have_http_status(204) end it 'returns 404 for non existing label' do @@ -317,10 +319,10 @@ describe API::Labels, api: true do end end - describe "POST /projects/:id/labels/:label_id/subscription" do + describe "POST /projects/:id/labels/:label_id/subscribe" do context "when label_id is a label title" do it "subscribes to the label" do - post api("/projects/#{project.id}/labels/#{label1.title}/subscription", user) + post api("/projects/#{project.id}/labels/#{label1.title}/subscribe", user) expect(response).to have_http_status(201) expect(json_response["name"]).to eq(label1.title) @@ -330,7 +332,7 @@ describe API::Labels, api: true do context "when label_id is a label ID" do it "subscribes to the label" do - post api("/projects/#{project.id}/labels/#{label1.id}/subscription", user) + post api("/projects/#{project.id}/labels/#{label1.id}/subscribe", user) expect(response).to have_http_status(201) expect(json_response["name"]).to eq(label1.title) @@ -342,7 +344,7 @@ describe API::Labels, api: true do before { label1.subscribe(user, project) } it "returns 304" do - post api("/projects/#{project.id}/labels/#{label1.id}/subscription", user) + post api("/projects/#{project.id}/labels/#{label1.id}/subscribe", user) expect(response).to have_http_status(304) end @@ -350,21 +352,21 @@ describe API::Labels, api: true do context "when label ID is not found" do it "returns 404 error" do - post api("/projects/#{project.id}/labels/1234/subscription", user) + post api("/projects/#{project.id}/labels/1234/subscribe", user) expect(response).to have_http_status(404) end end end - describe "DELETE /projects/:id/labels/:label_id/subscription" do + describe "POST /projects/:id/labels/:label_id/unsubscribe" do before { label1.subscribe(user, project) } context "when label_id is a label title" do it "unsubscribes from the label" do - delete api("/projects/#{project.id}/labels/#{label1.title}/subscription", user) + post api("/projects/#{project.id}/labels/#{label1.title}/unsubscribe", user) - expect(response).to have_http_status(200) + expect(response).to have_http_status(201) expect(json_response["name"]).to eq(label1.title) expect(json_response["subscribed"]).to be_falsey end @@ -372,9 +374,9 @@ describe API::Labels, api: true do context "when label_id is a label ID" do it "unsubscribes from the label" do - delete api("/projects/#{project.id}/labels/#{label1.id}/subscription", user) + post api("/projects/#{project.id}/labels/#{label1.id}/unsubscribe", user) - expect(response).to have_http_status(200) + expect(response).to have_http_status(201) expect(json_response["name"]).to eq(label1.title) expect(json_response["subscribed"]).to be_falsey end @@ -384,7 +386,7 @@ describe API::Labels, api: true do before { label1.unsubscribe(user, project) } it "returns 304" do - delete api("/projects/#{project.id}/labels/#{label1.id}/subscription", user) + post api("/projects/#{project.id}/labels/#{label1.id}/unsubscribe", user) expect(response).to have_http_status(304) end @@ -392,7 +394,7 @@ describe API::Labels, api: true do context "when label ID is not found" do it "returns 404 error" do - delete api("/projects/#{project.id}/labels/1234/subscription", user) + post api("/projects/#{project.id}/labels/1234/unsubscribe", user) expect(response).to have_http_status(404) end diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb index 9892e014cb9..2d37d026a39 100644 --- a/spec/requests/api/members_spec.rb +++ b/spec/requests/api/members_spec.rb @@ -34,9 +34,12 @@ describe API::Members, api: true do context "when authenticated as a #{type}" do it 'returns 200' do user = public_send(type) + get api("/#{source_type.pluralize}/#{source.id}/members", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array expect(json_response.size).to eq(2) expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id] end @@ -49,6 +52,8 @@ describe API::Members, api: true do get api("/#{source_type.pluralize}/#{source.id}/members", developer) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array expect(json_response.size).to eq(2) expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id] end @@ -57,6 +62,8 @@ describe API::Members, api: true do get api("/#{source_type.pluralize}/#{source.id}/members", developer), query: master.username expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array expect(json_response.count).to eq(1) expect(json_response.first['username']).to eq(master.username) end @@ -145,11 +152,11 @@ describe API::Members, api: true do end end - it "returns #{source_type == 'project' ? 201 : 409} if member already exists" do + it "returns 409 if member already exists" do post api("/#{source_type.pluralize}/#{source.id}/members", master), user_id: master.id, access_level: Member::MASTER - expect(response).to have_http_status(source_type == 'project' ? 201 : 409) + expect(response).to have_http_status(409) end it 'returns 400 when user_id is not given' do @@ -166,11 +173,11 @@ describe API::Members, api: true do expect(response).to have_http_status(400) end - it 'returns 422 when access_level is not valid' do + it 'returns 400 when access_level is not valid' do post api("/#{source_type.pluralize}/#{source.id}/members", master), user_id: stranger.id, access_level: 1234 - expect(response).to have_http_status(422) + expect(response).to have_http_status(400) end end end @@ -223,11 +230,11 @@ describe API::Members, api: true do expect(response).to have_http_status(400) end - it 'returns 422 when access level is not valid' do + it 'returns 400 when access level is not valid' do put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master), access_level: 1234 - expect(response).to have_http_status(422) + expect(response).to have_http_status(400) end end end @@ -256,18 +263,18 @@ describe API::Members, api: true do expect do delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", developer) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) end.to change { source.members.count }.by(-1) end end context 'when authenticated as a master/owner' do context 'and member is a requester' do - it "returns #{source_type == 'project' ? 200 : 404}" do + it 'returns 404' do expect do delete api("/#{source_type.pluralize}/#{source.id}/members/#{access_requester.id}", master) - expect(response).to have_http_status(source_type == 'project' ? 200 : 404) + expect(response).to have_http_status(404) end.not_to change { source.requesters.count } end end @@ -276,15 +283,15 @@ describe API::Members, api: true do expect do delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) end.to change { source.members.count }.by(-1) end end - it "returns #{source_type == 'project' ? 200 : 404} if member does not exist" do + it 'returns 404 if member does not exist' do delete api("/#{source_type.pluralize}/#{source.id}/members/123", master) - expect(response).to have_http_status(source_type == 'project' ? 200 : 404) + expect(response).to have_http_status(404) end end end @@ -335,7 +342,7 @@ describe API::Members, api: true do post api("/projects/#{project.id}/members", master), user_id: stranger.id, access_level: Member::OWNER - expect(response).to have_http_status(422) + expect(response).to have_http_status(400) end.to change { project.members.count }.by(0) end end diff --git a/spec/requests/api/merge_request_diffs_spec.rb b/spec/requests/api/merge_request_diffs_spec.rb index e1887138aab..1d02e827183 100644 --- a/spec/requests/api/merge_request_diffs_spec.rb +++ b/spec/requests/api/merge_request_diffs_spec.rb @@ -19,6 +19,8 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs', api: true do merge_request_diff = merge_request.merge_request_diffs.first expect(response.status).to eq 200 + expect(response).to include_pagination_headers + expect(json_response).to be_an Array expect(json_response.size).to eq(merge_request.merge_request_diffs.size) expect(json_response.first['id']).to eq(merge_request_diff.id) expect(json_response.first['head_commit_sha']).to eq(merge_request_diff.head_commit_sha) diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index ff10e79e417..b3f0876c822 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -27,7 +27,9 @@ describe API::MergeRequests, api: true do context "when authenticated" do it "returns an array of all merge_requests" do get api("/projects/#{project.id}/merge_requests", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) expect(json_response.last['title']).to eq(merge_request.title) @@ -43,7 +45,9 @@ describe API::MergeRequests, api: true do it "returns an array of all merge_requests" do get api("/projects/#{project.id}/merge_requests?state", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) expect(json_response.last['title']).to eq(merge_request.title) @@ -51,7 +55,9 @@ describe API::MergeRequests, api: true do it "returns an array of open merge_requests" do get api("/projects/#{project.id}/merge_requests?state=opened", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.last['title']).to eq(merge_request.title) @@ -59,7 +65,9 @@ describe API::MergeRequests, api: true do it "returns an array of closed merge_requests" do get api("/projects/#{project.id}/merge_requests?state=closed", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['title']).to eq(merge_request_closed.title) @@ -67,7 +75,9 @@ describe API::MergeRequests, api: true do it "returns an array of merged merge_requests" do get api("/projects/#{project.id}/merge_requests?state=merged", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['title']).to eq(merge_request_merged.title) @@ -91,7 +101,9 @@ describe API::MergeRequests, api: true do it "returns an array of merge_requests in ascending order" do get api("/projects/#{project.id}/merge_requests?sort=asc", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) response_dates = json_response.map{ |merge_request| merge_request['created_at'] } @@ -100,7 +112,9 @@ describe API::MergeRequests, api: true do it "returns an array of merge_requests in descending order" do get api("/projects/#{project.id}/merge_requests?sort=desc", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) response_dates = json_response.map{ |merge_request| merge_request['created_at'] } @@ -109,7 +123,9 @@ describe API::MergeRequests, api: true do it "returns an array of merge_requests ordered by updated_at" do get api("/projects/#{project.id}/merge_requests?order_by=updated_at", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) response_dates = json_response.map{ |merge_request| merge_request['updated_at'] } @@ -118,7 +134,9 @@ describe API::MergeRequests, api: true do it "returns an array of merge_requests ordered by created_at" do get api("/projects/#{project.id}/merge_requests?order_by=created_at&sort=asc", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) response_dates = json_response.map{ |merge_request| merge_request['created_at'] } @@ -152,7 +170,7 @@ describe API::MergeRequests, api: true do expect(json_response['source_project_id']).to eq(merge_request.source_project.id) expect(json_response['target_project_id']).to eq(merge_request.target_project.id) expect(json_response['work_in_progress']).to be_falsy - expect(json_response['merge_when_build_succeeds']).to be_falsy + expect(json_response['merge_when_pipeline_succeeds']).to be_falsy expect(json_response['merge_status']).to eq('can_be_merged') expect(json_response['should_close_merge_request']).to be_falsy expect(json_response['force_close_merge_request']).to be_falsy @@ -191,6 +209,8 @@ describe API::MergeRequests, api: true do commit = merge_request.commits.first expect(response.status).to eq 200 + expect(response).to include_pagination_headers + expect(json_response).to be_an Array expect(json_response.size).to eq(merge_request.commits.size) expect(json_response.first['id']).to eq(commit.id) expect(json_response.first['title']).to eq(commit.title) @@ -205,6 +225,7 @@ describe API::MergeRequests, api: true do describe 'GET /projects/:id/merge_requests/:merge_request_id/changes' do it 'returns the change information of the merge_request' do get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/changes", user) + expect(response.status).to eq 200 expect(json_response['changes'].size).to eq(merge_request.diffs.size) end @@ -229,7 +250,7 @@ describe API::MergeRequests, api: true do expect(response).to have_http_status(201) expect(json_response['title']).to eq('Test merge_request') - expect(json_response['labels']).to eq(['label', 'label2']) + expect(json_response['labels']).to eq(%w(label label2)) expect(json_response['milestone']['id']).to eq(milestone.id) expect(json_response['force_remove_source_branch']).to be_truthy end @@ -390,7 +411,7 @@ describe API::MergeRequests, api: true do it "destroys the merge request owners can destroy" do delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) end end end @@ -462,11 +483,11 @@ describe API::MergeRequests, api: true do allow_any_instance_of(MergeRequest).to receive(:head_pipeline).and_return(pipeline) allow(pipeline).to receive(:active?).and_return(true) - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), merge_when_build_succeeds: true + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), merge_when_pipeline_succeeds: true expect(response).to have_http_status(200) expect(json_response['title']).to eq('Test') - expect(json_response['merge_when_build_succeeds']).to eq(true) + expect(json_response['merge_when_pipeline_succeeds']).to eq(true) end end @@ -572,7 +593,9 @@ describe API::MergeRequests, api: true do it "returns merge_request comments ordered by created_at" do get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) expect(json_response.first['note']).to eq("a comment on a MR") @@ -594,7 +617,9 @@ describe API::MergeRequests, api: true do end get api("/projects/#{project.id}/merge_requests/#{mr.id}/closes_issues", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(issue.id) @@ -602,7 +627,9 @@ describe API::MergeRequests, api: true do it 'returns an empty array when there are no issues to be closed' do get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) end @@ -616,6 +643,7 @@ describe API::MergeRequests, api: true do get api("/projects/#{jira_project.id}/merge_requests/#{merge_request.id}/closes_issues", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['title']).to eq(issue.title) @@ -634,22 +662,22 @@ describe API::MergeRequests, api: true do end end - describe 'POST :id/merge_requests/:merge_request_id/subscription' do + describe 'POST :id/merge_requests/:merge_request_id/subscribe' do it 'subscribes to a merge request' do - post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin) + post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscribe", admin) expect(response).to have_http_status(201) expect(json_response['subscribed']).to eq(true) end it 'returns 304 if already subscribed' do - post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user) + post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscribe", user) expect(response).to have_http_status(304) end it 'returns 404 if the merge request is not found' do - post api("/projects/#{project.id}/merge_requests/123/subscription", user) + post api("/projects/#{project.id}/merge_requests/123/subscribe", user) expect(response).to have_http_status(404) end @@ -658,28 +686,28 @@ describe API::MergeRequests, api: true do guest = create(:user) project.team << [guest, :guest] - post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest) + post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscribe", guest) expect(response).to have_http_status(403) end end - describe 'DELETE :id/merge_requests/:merge_request_id/subscription' do + describe 'POST :id/merge_requests/:merge_request_id/unsubscribe' do it 'unsubscribes from a merge request' do - delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user) + post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/unsubscribe", user) - expect(response).to have_http_status(200) + expect(response).to have_http_status(201) expect(json_response['subscribed']).to eq(false) end it 'returns 304 if not subscribed' do - delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin) + post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/unsubscribe", admin) expect(response).to have_http_status(304) end it 'returns 404 if the merge request is not found' do - post api("/projects/#{project.id}/merge_requests/123/subscription", user) + post api("/projects/#{project.id}/merge_requests/123/unsubscribe", user) expect(response).to have_http_status(404) end @@ -688,7 +716,7 @@ describe API::MergeRequests, api: true do guest = create(:user) project.team << [guest, :guest] - delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest) + post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/unsubscribe", guest) expect(response).to have_http_status(403) end diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb index 8beef821d6c..78c230117b8 100644 --- a/spec/requests/api/milestones_spec.rb +++ b/spec/requests/api/milestones_spec.rb @@ -4,8 +4,8 @@ describe API::Milestones, api: true do include ApiHelpers let(:user) { create(:user) } let!(:project) { create(:empty_project, namespace: user.namespace ) } - let!(:closed_milestone) { create(:closed_milestone, project: project) } - let!(:milestone) { create(:milestone, project: project) } + let!(:closed_milestone) { create(:closed_milestone, project: project, title: 'version1', description: 'closed milestone') } + let!(:milestone) { create(:milestone, project: project, title: 'version2', description: 'open milestone') } before { project.team << [user, :developer] } @@ -14,6 +14,7 @@ describe API::Milestones, api: true do get api("/projects/#{project.id}/milestones", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['title']).to eq(milestone.title) end @@ -28,6 +29,7 @@ describe API::Milestones, api: true do get api("/projects/#{project.id}/milestones?state=active", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(milestone.id) @@ -37,10 +39,30 @@ describe API::Milestones, api: true do get api("/projects/#{project.id}/milestones?state=closed", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(closed_milestone.id) end + + it 'returns an array of milestones specified by iids' do + other_milestone = create(:milestone, project: project) + + get api("/projects/#{project.id}/milestones", user), iids: [closed_milestone.iid, other_milestone.iid] + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.map{ |m| m['id'] }).to match_array([closed_milestone.id, other_milestone.id]) + end + + it 'does not return any milestone if none found' do + get api("/projects/#{project.id}/milestones", user), iids: [Milestone.maximum(:iid).succ] + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end end describe 'GET /projects/:id/milestones/:milestone_id' do @@ -52,23 +74,46 @@ describe API::Milestones, api: true do expect(json_response['iid']).to eq(milestone.iid) end - it 'returns a project milestone by iid' do - get api("/projects/#{project.id}/milestones?iid=#{closed_milestone.iid}", user) + it 'returns a project milestone by iids array' do + get api("/projects/#{project.id}/milestones?iids=#{closed_milestone.iid}", user) expect(response.status).to eq 200 + expect(response).to include_pagination_headers + expect(json_response.size).to eq(1) expect(json_response.size).to eq(1) expect(json_response.first['title']).to eq closed_milestone.title expect(json_response.first['id']).to eq closed_milestone.id end - it 'returns a project milestone by iid array' do - get api("/projects/#{project.id}/milestones", user), iid: [milestone.iid, closed_milestone.iid] + it 'returns a project milestone by searching for title' do + get api("/projects/#{project.id}/milestones", user), search: 'version2' + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response.size).to eq(1) + expect(json_response.first['title']).to eq milestone.title + expect(json_response.first['id']).to eq milestone.id + end + + it 'returns a project milestones by searching for description' do + get api("/projects/#{project.id}/milestones", user), search: 'open' expect(response).to have_http_status(200) - expect(json_response.size).to eq(2) + expect(response).to include_pagination_headers + expect(json_response.size).to eq(1) expect(json_response.first['title']).to eq milestone.title expect(json_response.first['id']).to eq milestone.id end + end + + describe 'GET /projects/:id/milestones/:milestone_id' do + it 'returns a project milestone by id' do + get api("/projects/#{project.id}/milestones/#{milestone.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['title']).to eq(milestone.title) + expect(json_response['iid']).to eq(milestone.iid) + end it 'returns 401 error if user not authenticated' do get api("/projects/#{project.id}/milestones/#{milestone.id}") @@ -177,6 +222,7 @@ describe API::Milestones, api: true do get api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['milestone']['title']).to eq(milestone.title) end @@ -202,6 +248,7 @@ describe API::Milestones, api: true do get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(2) expect(json_response.map { |issue| issue['id'] }).to include(issue.id, confidential_issue.id) @@ -214,6 +261,7 @@ describe API::Milestones, api: true do get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", member) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(1) expect(json_response.map { |issue| issue['id'] }).to include(issue.id) @@ -223,10 +271,47 @@ describe API::Milestones, api: true do get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", create(:user)) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(1) expect(json_response.map { |issue| issue['id'] }).to include(issue.id) end end end + + describe 'GET /projects/:id/milestones/:milestone_id/merge_requests' do + let(:merge_request) { create(:merge_request, source_project: project) } + before do + milestone.merge_requests << merge_request + end + + it 'returns project merge_requests for a particular milestone' do + get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + expect(json_response.first['title']).to eq(merge_request.title) + expect(json_response.first['milestone']['title']).to eq(milestone.title) + end + + it 'returns a 404 error if milestone id not found' do + get api("/projects/#{project.id}/milestones/1234/merge_requests", user) + + expect(response).to have_http_status(404) + end + + it 'returns a 404 if the user has no access to the milestone' do + new_user = create :user + get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests", new_user) + + expect(response).to have_http_status(404) + end + + it 'returns a 401 error if user not authenticated' do + get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests") + + expect(response).to have_http_status(401) + end + end end diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb index c1edf384d5c..da8fa06d0af 100644 --- a/spec/requests/api/namespaces_spec.rb +++ b/spec/requests/api/namespaces_spec.rb @@ -5,7 +5,7 @@ describe API::Namespaces, api: true do let(:admin) { create(:admin) } let(:user) { create(:user) } let!(:group1) { create(:group) } - let!(:group2) { create(:group) } + let!(:group2) { create(:group, :nested) } describe "GET /namespaces" do context "when unauthenticated" do @@ -18,35 +18,41 @@ describe API::Namespaces, api: true do context "when authenticated as admin" do it "admin: returns an array of all namespaces" do get api("/namespaces", admin) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.length).to eq(Namespace.count) end it "admin: returns an array of matched namespaces" do - get api("/namespaces?search=#{group1.name}", admin) + get api("/namespaces?search=#{group2.name}", admin) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.length).to eq(1) + expect(json_response.last['path']).to eq(group2.path) + expect(json_response.last['full_path']).to eq(group2.full_path) end end context "when authenticated as a regular user" do it "user: returns an array of namespaces" do get api("/namespaces", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.length).to eq(1) end it "admin: returns an array of matched namespaces" do get api("/namespaces?search=#{user.username}", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.length).to eq(1) end end diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index 0353ebea9e5..347f8f6fa3b 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -32,15 +32,12 @@ describe API::Notes, api: true do before { project.team << [user, :reporter] } describe "GET /projects/:id/noteable/:noteable_id/notes" do - it_behaves_like 'a paginated resources' do - let(:request) { get api("/projects/#{project.id}/issues/#{issue.id}/notes", user) } - end - context "when noteable is an Issue" do it "returns an array of issue notes" do get api("/projects/#{project.id}/issues/#{issue.id}/notes", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['body']).to eq(issue_note.note) end @@ -56,6 +53,7 @@ describe API::Notes, api: true do get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response).to be_empty end @@ -75,6 +73,7 @@ describe API::Notes, api: true do get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", private_user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['body']).to eq(cross_reference_note.note) end @@ -87,6 +86,7 @@ describe API::Notes, api: true do get api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['body']).to eq(snippet_note.note) end @@ -109,6 +109,7 @@ describe API::Notes, api: true do get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['body']).to eq(merge_request_note.note) end @@ -224,11 +225,11 @@ describe API::Notes, api: true do context 'when the user is posting an award emoji on an issue created by someone else' do let(:issue2) { create(:issue, project: project) } - it 'returns an award emoji' do + it 'creates a new issue note' do post api("/projects/#{project.id}/issues/#{issue2.id}/notes", user), body: ':+1:' expect(response).to have_http_status(201) - expect(json_response['awardable_id']).to eq issue2.id + expect(json_response['body']).to eq(':+1:') end end @@ -372,7 +373,7 @@ describe API::Notes, api: true do delete api("/projects/#{project.id}/issues/#{issue.id}/"\ "notes/#{issue_note.id}", user) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) # Check if note is really deleted delete api("/projects/#{project.id}/issues/#{issue.id}/"\ "notes/#{issue_note.id}", user) @@ -391,7 +392,7 @@ describe API::Notes, api: true do delete api("/projects/#{project.id}/snippets/#{snippet.id}/"\ "notes/#{snippet_note.id}", user) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) # Check if note is really deleted delete api("/projects/#{project.id}/snippets/#{snippet.id}/"\ "notes/#{snippet_note.id}", user) @@ -411,7 +412,7 @@ describe API::Notes, api: true do delete api("/projects/#{project.id}/merge_requests/"\ "#{merge_request.id}/notes/#{merge_request_note.id}", user) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) # Check if note is really deleted delete api("/projects/#{project.id}/merge_requests/"\ "#{merge_request.id}/notes/#{merge_request_note.id}", user) diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb index b7a0b5a9e13..51af999b455 100644 --- a/spec/requests/api/pipelines_spec.rb +++ b/spec/requests/api/pipelines_spec.rb @@ -15,18 +15,16 @@ describe API::Pipelines, api: true do before { project.team << [user, :master] } describe 'GET /projects/:id/pipelines ' do - it_behaves_like 'a paginated resources' do - let(:request) { get api("/projects/#{project.id}/pipelines", user) } - end - context 'authorized user' do it 'returns project pipelines' do get api("/projects/#{project.id}/pipelines", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['sha']).to match /\A\h{40}\z/ expect(json_response.first['id']).to eq pipeline.id + expect(json_response.first.keys).to contain_exactly(*%w[id sha ref status]) end end diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb index f4973d71088..f286568547d 100644 --- a/spec/requests/api/project_hooks_spec.rb +++ b/spec/requests/api/project_hooks_spec.rb @@ -25,6 +25,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do expect(response).to have_http_status(200) expect(json_response).to be_an Array + expect(response).to include_pagination_headers expect(json_response.count).to eq(1) expect(json_response.first['url']).to eq("http://example.com") expect(json_response.first['issues_events']).to eq(true) @@ -182,13 +183,9 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do it "deletes hook from project" do expect do delete api("/projects/#{project.id}/hooks/#{hook.id}", user) - end.to change {project.hooks.count}.by(-1) - expect(response).to have_http_status(200) - end - it "returns success when deleting hook" do - delete api("/projects/#{project.id}/hooks/#{hook.id}", user) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) + end.to change {project.hooks.count}.by(-1) end it "returns a 404 error when deleting non existent hook" do diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index eea76c7bb94..9e88c19b0bc 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -16,9 +16,11 @@ describe API::ProjectSnippets, api: true do internal_snippet = create(:project_snippet, :internal, project: project) private_snippet = create(:project_snippet, :private, project: project) - get api("/projects/#{project.id}/snippets/", user) + get api("/projects/#{project.id}/snippets", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array expect(json_response.size).to eq(3) expect(json_response.map{ |snippet| snippet['id']} ).to include(public_snippet.id, internal_snippet.id, private_snippet.id) expect(json_response.last).to have_key('web_url') @@ -28,7 +30,10 @@ describe API::ProjectSnippets, api: true do create(:project_snippet, :private, project: project) get api("/projects/#{project.id}/snippets/", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array expect(json_response.size).to eq(0) end end @@ -39,7 +44,7 @@ describe API::ProjectSnippets, api: true do title: 'Test Title', file_name: 'test.rb', code: 'puts "hello world"', - visibility_level: Snippet::PUBLIC + visibility: 'public' } end @@ -51,7 +56,7 @@ describe API::ProjectSnippets, api: true do expect(snippet.content).to eq(params[:code]) expect(snippet.title).to eq(params[:title]) expect(snippet.file_name).to eq(params[:file_name]) - expect(snippet.visibility_level).to eq(params[:visibility_level]) + expect(snippet.visibility_level).to eq(Snippet::PUBLIC) end it 'returns 400 for missing parameters' do @@ -73,43 +78,33 @@ describe API::ProjectSnippets, api: true do allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) end - context 'when the project is private' do - let(:private_project) { create(:project_empty_repo, :private) } - - context 'when the snippet is public' do - it 'creates the snippet' do - expect { create_snippet(private_project, visibility_level: Snippet::PUBLIC) }. - to change { Snippet.count }.by(1) - end + context 'when the snippet is private' do + it 'creates the snippet' do + expect { create_snippet(project, visibility: 'private') }. + to change { Snippet.count }.by(1) end end - context 'when the project is public' do - context 'when the snippet is private' do - it 'creates the snippet' do - expect { create_snippet(project, visibility_level: Snippet::PRIVATE) }. - to change { Snippet.count }.by(1) - end + context 'when the snippet is public' do + it 'rejects the snippet' do + expect { create_snippet(project, visibility: 'public') }. + not_to change { Snippet.count } + + expect(response).to have_http_status(400) + expect(json_response['message']).to eq({ "error" => "Spam detected" }) end - context 'when the snippet is public' do - it 'rejects the shippet' do - expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }. - not_to change { Snippet.count } - expect(response).to have_http_status(400) - end - - it 'creates a spam log' do - expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }. - to change { SpamLog.count }.by(1) - end + it 'creates a spam log' do + expect { create_snippet(project, visibility: 'public') }. + to change { SpamLog.count }.by(1) end end end end describe 'PUT /projects/:project_id/snippets/:id/' do - let(:snippet) { create(:project_snippet, author: admin) } + let(:visibility_level) { Snippet::PUBLIC } + let(:snippet) { create(:project_snippet, author: admin, visibility_level: visibility_level) } it 'updates snippet' do new_content = 'New content' @@ -133,6 +128,56 @@ describe API::ProjectSnippets, api: true do expect(response).to have_http_status(400) end + + context 'when the snippet is spam' do + def update_snippet(snippet_params = {}) + put api("/projects/#{snippet.project.id}/snippets/#{snippet.id}", admin), snippet_params + end + + before do + allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) + end + + context 'when the snippet is private' do + let(:visibility_level) { Snippet::PRIVATE } + + it 'creates the snippet' do + expect { update_snippet(title: 'Foo') }. + to change { snippet.reload.title }.to('Foo') + end + end + + context 'when the snippet is public' do + let(:visibility_level) { Snippet::PUBLIC } + + it 'rejects the snippet' do + expect { update_snippet(title: 'Foo') }. + not_to change { snippet.reload.title } + end + + it 'creates a spam log' do + expect { update_snippet(title: 'Foo') }. + to change { SpamLog.count }.by(1) + end + end + + context 'when the private snippet is made public' do + let(:visibility_level) { Snippet::PRIVATE } + + it 'rejects the snippet' do + expect { update_snippet(title: 'Foo', visibility: 'public') }. + not_to change { snippet.reload.title } + + expect(response).to have_http_status(400) + expect(json_response['message']).to eq({ "error" => "Spam detected" }) + end + + it 'creates a spam log' do + expect { update_snippet(title: 'Foo', visibility: 'public') }. + to change { SpamLog.count }.by(1) + end + end + end end describe 'DELETE /projects/:project_id/snippets/:id/' do @@ -144,7 +189,7 @@ describe API::ProjectSnippets, api: true do delete api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) end it 'returns 404 for invalid snippet id' do @@ -167,7 +212,7 @@ describe API::ProjectSnippets, api: true do end it 'returns 404 for invalid snippet id' do - delete api("/projects/#{snippet.project.id}/snippets/1234", admin) + get api("/projects/#{snippet.project.id}/snippets/1234/raw", admin) expect(response).to have_http_status(404) expect(json_response['message']).to eq('404 Snippet Not Found') diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index ac0bbec44e0..03cae074803 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -41,261 +41,246 @@ describe API::Projects, api: true do end describe 'GET /projects' do - before { project } + shared_examples_for 'projects response' do + it 'returns an array of projects' do + get api('/projects', current_user), filter + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.map { |p| p['id'] }).to contain_exactly(*projects.map(&:id)) + end + end + + let!(:public_project) { create(:empty_project, :public, name: 'public_project') } + before do + project + project2 + project3 + project4 + end context 'when unauthenticated' do - it 'returns authentication error' do - get api('/projects') - expect(response).to have_http_status(401) + it_behaves_like 'projects response' do + let(:filter) { {} } + let(:current_user) { nil } + let(:projects) { [public_project] } end end context 'when authenticated as regular user' do - it 'returns an array of projects' do - get api('/projects', user) - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['name']).to eq(project.name) - expect(json_response.first['owner']['username']).to eq(user.username) + it_behaves_like 'projects response' do + let(:filter) { {} } + let(:current_user) { user } + let(:projects) { [public_project, project, project2, project3] } end it 'includes the project labels as the tag_list' do get api('/projects', user) + expect(response.status).to eq 200 + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first.keys).to include('tag_list') end it 'includes open_issues_count' do get api('/projects', user) + expect(response.status).to eq 200 + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first.keys).to include('open_issues_count') end - it 'does not include open_issues_count' do + it 'does not include open_issues_count if issues are disabled' do project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) get api('/projects', user) + expect(response.status).to eq 200 + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.find { |hash| hash['id'] == project.id }.keys).not_to include('open_issues_count') + end + + it "does not include statistics by default" do + get api('/projects', user) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first).not_to include('statistics') + end + + it "includes statistics if requested" do + get api('/projects', user), statistics: true + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.first.keys).not_to include('open_issues_count') + expect(json_response.first).to include 'statistics' end - context 'GET /projects?simple=true' do + context 'and with simple=true' do it 'returns a simplified version of all the projects' do - expected_keys = ["id", "http_url_to_repo", "web_url", "name", "name_with_namespace", "path", "path_with_namespace"] + expected_keys = %w(id http_url_to_repo web_url name name_with_namespace path path_with_namespace) get api('/projects?simple=true', user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first.keys).to match_array expected_keys end end context 'and using search' do - it 'returns searched project' do - get api('/projects', user), { search: project.name } - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) + it_behaves_like 'projects response' do + let(:filter) { { search: project.name } } + let(:current_user) { user } + let(:projects) { [project] } + end + end + + context 'and membership=true' do + it_behaves_like 'projects response' do + let(:filter) { { membership: true } } + let(:current_user) { user } + let(:projects) { [project, project2, project3] } end end context 'and using the visibility filter' do it 'filters based on private visibility param' do get api('/projects', user), { visibility: 'private' } + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PRIVATE).count) + expect(json_response.map { |p| p['id'] }).to contain_exactly(project.id, project2.id, project3.id) end it 'filters based on internal visibility param' do + project2.update_attribute(:visibility_level, Gitlab::VisibilityLevel::INTERNAL) + get api('/projects', user), { visibility: 'internal' } + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::INTERNAL).count) + expect(json_response.map { |p| p['id'] }).to contain_exactly(project2.id) end it 'filters based on public visibility param' do get api('/projects', user), { visibility: 'public' } + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PUBLIC).count) + expect(json_response.map { |p| p['id'] }).to contain_exactly(public_project.id) end end context 'and using sorting' do - before do - project2 - project3 - end - it 'returns the correct order when sorted by id' do get api('/projects', user), { order_by: 'id', sort: 'desc' } + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['id']).to eq(project3.id) end end - end - end - - describe 'GET /projects/all' do - before { project } - context 'when unauthenticated' do - it 'returns authentication error' do - get api('/projects/all') - expect(response).to have_http_status(401) - end - end + context 'and with owned=true' do + it 'returns an array of projects the user owns' do + get api('/projects', user4), owned: true - context 'when authenticated as regular user' do - it 'returns authentication error' do - get api('/projects/all', user) - expect(response).to have_http_status(403) - end - end - - context 'when authenticated as admin' do - it 'returns an array of all projects' do - get api('/projects/all', admin) - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - - expect(json_response).to satisfy do |response| - response.one? do |entry| - entry.has_key?('permissions') && - entry['name'] == project.name && - entry['owner']['username'] == user.username - end + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(project4.name) + expect(json_response.first['owner']['username']).to eq(user4.username) end end - it "does not include statistics by default" do - get api('/projects/all', admin) - - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first).not_to include('statistics') - end - - it "includes statistics if requested" do - get api('/projects/all', admin), statistics: true - - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first).to include 'statistics' - end - end - end + context 'and with starred=true' do + let(:public_project) { create(:empty_project, :public) } - describe 'GET /projects/owned' do - before do - project3 - project4 - end - - context 'when unauthenticated' do - it 'returns authentication error' do - get api('/projects/owned') - expect(response).to have_http_status(401) - end - end - - context 'when authenticated as project owner' do - it 'returns an array of projects the user owns' do - get api('/projects/owned', user4) - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['name']).to eq(project4.name) - expect(json_response.first['owner']['username']).to eq(user4.username) - end + before do + project_member2 + user3.update_attributes(starred_projects: [project, project2, project3, public_project]) + end - it "does not include statistics by default" do - get api('/projects/owned', user4) + it 'returns the starred projects viewable by the user' do + get api('/projects', user3), starred: true - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first).not_to include('statistics') + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.map { |project| project['id'] }).to contain_exactly(project.id, public_project.id) + end end - it "includes statistics if requested" do - attributes = { - commit_count: 23, - storage_size: 702, - repository_size: 123, - lfs_objects_size: 234, - build_artifacts_size: 345, - } - - project4.statistics.update!(attributes) - - get api('/projects/owned', user4), statistics: true + context 'and with all query parameters' do + let!(:project5) { create(:empty_project, :public, path: 'gitlab5', namespace: create(:namespace)) } + let!(:project6) { create(:empty_project, :public, path: 'project6', namespace: user.namespace) } + let!(:project7) { create(:empty_project, :public, path: 'gitlab7', namespace: user.namespace) } + let!(:project8) { create(:empty_project, path: 'gitlab8', namespace: user.namespace) } + let!(:project9) { create(:empty_project, :public, path: 'gitlab9') } - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['statistics']).to eq attributes.stringify_keys - end - end - end + before do + user.update_attributes(starred_projects: [project5, project7, project8, project9]) + end - describe 'GET /projects/visible' do - shared_examples_for 'visible projects response' do - it 'returns the visible projects' do - get api('/projects/visible', current_user) + context 'including owned filter' do + it 'returns only projects that satisfy all query parameters' do + get api('/projects', user), { visibility: 'public', owned: true, starred: true, search: 'gitlab' } - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.map { |p| p['id'] }).to contain_exactly(*projects.map(&:id)) - end - end + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + expect(json_response.first['id']).to eq(project7.id) + end + end - let!(:public_project) { create(:empty_project, :public) } - before do - project - project2 - project3 - project4 - end + context 'including membership filter' do + before do + create(:project_member, + user: user, + project: project5, + access_level: ProjectMember::MASTER) + end - context 'when unauthenticated' do - it_behaves_like 'visible projects response' do - let(:current_user) { nil } - let(:projects) { [public_project] } - end - end + it 'returns only projects that satisfy all query parameters' do + get api('/projects', user), { visibility: 'public', membership: true, starred: true, search: 'gitlab' } - context 'when authenticated' do - it_behaves_like 'visible projects response' do - let(:current_user) { user } - let(:projects) { [public_project, project, project2, project3] } + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.size).to eq(2) + expect(json_response.map { |project| project['id'] }).to contain_exactly(project5.id, project7.id) + end + end end end context 'when authenticated as a different user' do - it_behaves_like 'visible projects response' do + it_behaves_like 'projects response' do + let(:filter) { {} } let(:current_user) { user2 } let(:projects) { [public_project] } end end - end - describe 'GET /projects/starred' do - let(:public_project) { create(:empty_project, :public) } - - before do - project_member2 - user3.update_attributes(starred_projects: [project, project2, project3, public_project]) - end - - it 'returns the starred projects viewable by the user' do - get api('/projects/starred', user3) - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.map { |project| project['id'] }).to contain_exactly(project.id, public_project.id) + context 'when authenticated as admin' do + it_behaves_like 'projects response' do + let(:filter) { {} } + let(:current_user) { admin } + let(:projects) { Project.all } + end end end @@ -309,10 +294,37 @@ describe API::Projects, api: true do end end - it 'creates new project without path and return 201' do - expect { post api('/projects', user), name: 'foo' }. + it 'creates new project without path but with name and returns 201' do + expect { post api('/projects', user), name: 'Foo Project' }. + to change { Project.count }.by(1) + expect(response).to have_http_status(201) + + project = Project.first + + expect(project.name).to eq('Foo Project') + expect(project.path).to eq('foo-project') + end + + it 'creates new project without name but with path and returns 201' do + expect { post api('/projects', user), path: 'foo_project' }. to change { Project.count }.by(1) expect(response).to have_http_status(201) + + project = Project.first + + expect(project.name).to eq('foo_project') + expect(project.path).to eq('foo_project') + end + + it 'creates new project name and path and returns 201' do + expect { post api('/projects', user), path: 'foo-Project', name: 'Foo Project' }. + to change { Project.count }.by(1) + expect(response).to have_http_status(201) + + project = Project.first + + expect(project.name).to eq('Foo Project') + expect(project.path).to eq('foo-Project') end it 'creates last project before reaching project limit' do @@ -321,7 +333,7 @@ describe API::Projects, api: true do expect(response).to have_http_status(201) end - it 'does not create new project without name and return 400' do + it 'does not create new project without name or path and returns 400' do expect { post api('/projects', user) }.not_to change { Project.count } expect(response).to have_http_status(400) end @@ -333,7 +345,7 @@ describe API::Projects, api: true do issues_enabled: false, merge_requests_enabled: false, wiki_enabled: false, - only_allow_merge_if_build_succeeds: false, + only_allow_merge_if_pipeline_succeeds: false, request_access_enabled: true, only_allow_merge_if_all_discussions_are_resolved: false }) @@ -353,36 +365,39 @@ describe API::Projects, api: true do end it 'sets a project as public' do - project = attributes_for(:project, :public) + project = attributes_for(:project, visibility: 'public') + post api('/projects', user), project - expect(json_response['public']).to be_truthy - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) + + expect(json_response['visibility']).to eq('public') end it 'sets a project as internal' do - project = attributes_for(:project, :internal) + project = attributes_for(:project, visibility: 'internal') + post api('/projects', user), project - expect(json_response['public']).to be_falsey - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) + + expect(json_response['visibility']).to eq('internal') end it 'sets a project as private' do - project = attributes_for(:project, :private) + project = attributes_for(:project, visibility: 'private') + post api('/projects', user), project - expect(json_response['public']).to be_falsey - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) + + expect(json_response['visibility']).to eq('private') end it 'sets a project as allowing merge even if build fails' do - project = attributes_for(:project, { only_allow_merge_if_build_succeeds: false }) + project = attributes_for(:project, { only_allow_merge_if_pipeline_succeeds: false }) post api('/projects', user), project - expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey + expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_falsey end - it 'sets a project as allowing merge only if build succeeds' do - project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true }) + it 'sets a project as allowing merge only if merge_when_pipeline_succeeds' do + project = attributes_for(:project, { only_allow_merge_if_pipeline_succeeds: true }) post api('/projects', user), project - expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy + expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_truthy end it 'sets a project as allowing merge even if discussions are unresolved' do @@ -410,7 +425,7 @@ describe API::Projects, api: true do end context 'when a visibility level is restricted' do - let(:project_param) { attributes_for(:project, :public) } + let(:project_param) { attributes_for(:project, visibility: 'public') } before do stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) @@ -428,10 +443,7 @@ describe API::Projects, api: true do it 'allows an admin to override restricted visibility settings' do post api('/projects', admin), project_param - expect(json_response['public']).to be_truthy - expect(json_response['visibility_level']).to( - eq(Gitlab::VisibilityLevel::PUBLIC) - ) + expect(json_response['visibility']).to eq('public') end end end @@ -472,40 +484,41 @@ describe API::Projects, api: true do end it 'sets a project as public' do - project = attributes_for(:project, :public) + project = attributes_for(:project, visibility: 'public') + post api("/projects/user/#{user.id}", admin), project expect(response).to have_http_status(201) - expect(json_response['public']).to be_truthy - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) + expect(json_response['visibility']).to eq('public') end it 'sets a project as internal' do - project = attributes_for(:project, :internal) + project = attributes_for(:project, visibility: 'internal') + post api("/projects/user/#{user.id}", admin), project expect(response).to have_http_status(201) - expect(json_response['public']).to be_falsey - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) + expect(json_response['visibility']).to eq('internal') end it 'sets a project as private' do - project = attributes_for(:project, :private) + project = attributes_for(:project, visibility: 'private') + post api("/projects/user/#{user.id}", admin), project - expect(json_response['public']).to be_falsey - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) + + expect(json_response['visibility']).to eq('private') end it 'sets a project as allowing merge even if build fails' do - project = attributes_for(:project, { only_allow_merge_if_build_succeeds: false }) + project = attributes_for(:project, { only_allow_merge_if_pipeline_succeeds: false }) post api("/projects/user/#{user.id}", admin), project - expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey + expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_falsey end - it 'sets a project as allowing merge only if build succeeds' do - project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true }) + it 'sets a project as allowing merge only if merge_when_pipeline_succeeds' do + project = attributes_for(:project, { only_allow_merge_if_pipeline_succeeds: true }) post api("/projects/user/#{user.id}", admin), project - expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy + expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_truthy end it 'sets a project as allowing merge even if discussions are unresolved' do @@ -569,9 +582,8 @@ describe API::Projects, api: true do expect(json_response['description']).to eq(project.description) expect(json_response['default_branch']).to eq(project.default_branch) expect(json_response['tag_list']).to be_an Array - expect(json_response['public']).to be_falsey expect(json_response['archived']).to be_falsey - expect(json_response['visibility_level']).to be_present + expect(json_response['visibility']).to be_present expect(json_response['ssh_url_to_repo']).to be_present expect(json_response['http_url_to_repo']).to be_present expect(json_response['web_url']).to be_present @@ -599,7 +611,7 @@ describe API::Projects, api: true do expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id) expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name) expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access) - expect(json_response['only_allow_merge_if_build_succeeds']).to eq(project.only_allow_merge_if_build_succeeds) + expect(json_response['only_allow_merge_if_pipeline_succeeds']).to eq(project.only_allow_merge_if_pipeline_succeeds) expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved) end @@ -639,6 +651,7 @@ describe API::Projects, api: true do 'name' => user.namespace.name, 'path' => user.namespace.path, 'kind' => user.namespace.kind, + 'full_path' => user.namespace.full_path, }) end @@ -697,9 +710,10 @@ describe API::Projects, api: true do get api("/projects/#{project.id}/events", current_user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array first_event = json_response.first - expect(first_event['action_name']).to eq('commented on') expect(first_event['note']['body']).to eq('What an awesome day!') @@ -752,11 +766,11 @@ describe API::Projects, api: true do get api("/projects/#{project.id}/users", current_user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(1) first_user = json_response.first - expect(first_user['username']).to eq(member.username) expect(first_user['name']).to eq(member.name) expect(first_user.keys).to contain_exactly(*%w[name username id state avatar_url web_url]) @@ -799,7 +813,9 @@ describe API::Projects, api: true do it 'returns an array of project snippets' do get api("/projects/#{project.id}/snippets", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['title']).to eq(snippet.title) end @@ -821,8 +837,7 @@ describe API::Projects, api: true do describe 'POST /projects/:id/snippets' do it 'creates a new project snippet' do post api("/projects/#{project.id}/snippets", user), - title: 'api test', file_name: 'sample.rb', code: 'test', - visibility_level: Gitlab::VisibilityLevel::PRIVATE + title: 'api test', file_name: 'sample.rb', code: 'test', visibility: 'private' expect(response).to have_http_status(201) expect(json_response['title']).to eq('api test') end @@ -856,8 +871,9 @@ describe API::Projects, api: true do it 'deletes existing project snippet' do expect do delete api("/projects/#{project.id}/snippets/#{snippet.id}", user) + + expect(response).to have_http_status(204) end.to change { Snippet.count }.by(-1) - expect(response).to have_http_status(200) end it 'returns 404 when deleting unknown snippet id' do @@ -941,8 +957,10 @@ describe API::Projects, api: true do project_fork_target.reload expect(project_fork_target.forked_from_project).not_to be_nil expect(project_fork_target.forked?).to be_truthy + delete api("/projects/#{project_fork_target.id}/fork", admin) - expect(response).to have_http_status(200) + + expect(response).to have_http_status(204) project_fork_target.reload expect(project_fork_target.forked_from_project).to be_nil expect(project_fork_target.forked?).not_to be_truthy @@ -1071,7 +1089,7 @@ describe API::Projects, api: true do end it 'updates visibility_level' do - project_param = { visibility_level: Gitlab::VisibilityLevel::PUBLIC } + project_param = { visibility: 'public' } put api("/projects/#{project3.id}", user), project_param expect(response).to have_http_status(200) project_param.each_pair do |k, v| @@ -1081,13 +1099,13 @@ describe API::Projects, api: true do it 'updates visibility_level from public to private' do project3.update_attributes({ visibility_level: Gitlab::VisibilityLevel::PUBLIC }) - project_param = { visibility_level: Gitlab::VisibilityLevel::PRIVATE } + project_param = { visibility: 'private' } put api("/projects/#{project3.id}", user), project_param expect(response).to have_http_status(200) project_param.each_pair do |k, v| expect(json_response[k.to_s]).to eq(v) end - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) + expect(json_response['visibility']).to eq('private') end it 'does not update name to existing name' do @@ -1154,7 +1172,7 @@ describe API::Projects, api: true do end it 'does not update visibility_level' do - project_param = { visibility_level: Gitlab::VisibilityLevel::PUBLIC } + project_param = { visibility: 'public' } put api("/projects/#{project3.id}", user4), project_param expect(response).to have_http_status(403) end @@ -1271,7 +1289,7 @@ describe API::Projects, api: true do end end - describe 'DELETE /projects/:id/star' do + describe 'POST /projects/:id/unstar' do context 'on a starred project' do before do user.toggle_star(project) @@ -1279,16 +1297,16 @@ describe API::Projects, api: true do end it 'unstars the project' do - expect { delete api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(-1) + expect { post api("/projects/#{project.id}/unstar", user) }.to change { project.reload.star_count }.by(-1) - expect(response).to have_http_status(200) + expect(response).to have_http_status(201) expect(json_response['star_count']).to eq(0) end end context 'on an unstarred project' do it 'does not modify the star count' do - expect { delete api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count } + expect { post api("/projects/#{project.id}/unstar", user) }.not_to change { project.reload.star_count } expect(response).to have_http_status(304) end @@ -1299,7 +1317,9 @@ describe API::Projects, api: true do context 'when authenticated as user' do it 'removes project' do delete api("/projects/#{project.id}", user) - expect(response).to have_http_status(200) + + expect(response).to have_http_status(202) + expect(json_response['message']).to eql('202 Accepted') end it 'does not remove a project if not an owner' do @@ -1323,7 +1343,9 @@ describe API::Projects, api: true do context 'when authenticated as admin' do it 'removes any existing project' do delete api("/projects/#{project.id}", admin) - expect(response).to have_http_status(200) + + expect(response).to have_http_status(202) + expect(json_response['message']).to eql('202 Accepted') end it 'does not remove a non existing project' do @@ -1332,4 +1354,179 @@ describe API::Projects, api: true do end end end + + describe 'POST /projects/:id/fork' do + let(:project) do + create(:project, :repository, creator: user, namespace: user.namespace) + end + let(:group) { create(:group) } + let(:group2) do + group = create(:group, name: 'group2_name') + group.add_owner(user2) + group + end + + before do + project.add_reporter(user2) + end + + context 'when authenticated' do + it 'forks if user has sufficient access to project' do + post api("/projects/#{project.id}/fork", user2) + + expect(response).to have_http_status(201) + expect(json_response['name']).to eq(project.name) + expect(json_response['path']).to eq(project.path) + expect(json_response['owner']['id']).to eq(user2.id) + expect(json_response['namespace']['id']).to eq(user2.namespace.id) + expect(json_response['forked_from_project']['id']).to eq(project.id) + end + + it 'forks if user is admin' do + post api("/projects/#{project.id}/fork", admin) + + expect(response).to have_http_status(201) + expect(json_response['name']).to eq(project.name) + expect(json_response['path']).to eq(project.path) + expect(json_response['owner']['id']).to eq(admin.id) + expect(json_response['namespace']['id']).to eq(admin.namespace.id) + expect(json_response['forked_from_project']['id']).to eq(project.id) + end + + it 'fails on missing project access for the project to fork' do + new_user = create(:user) + post api("/projects/#{project.id}/fork", new_user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Project Not Found') + end + + it 'fails if forked project exists in the user namespace' do + post api("/projects/#{project.id}/fork", user) + + expect(response).to have_http_status(409) + expect(json_response['message']['name']).to eq(['has already been taken']) + expect(json_response['message']['path']).to eq(['has already been taken']) + end + + it 'fails if project to fork from does not exist' do + post api('/projects/424242/fork', user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Project Not Found') + end + + it 'forks with explicit own user namespace id' do + post api("/projects/#{project.id}/fork", user2), namespace: user2.namespace.id + + expect(response).to have_http_status(201) + expect(json_response['owner']['id']).to eq(user2.id) + end + + it 'forks with explicit own user name as namespace' do + post api("/projects/#{project.id}/fork", user2), namespace: user2.username + + expect(response).to have_http_status(201) + expect(json_response['owner']['id']).to eq(user2.id) + end + + it 'forks to another user when admin' do + post api("/projects/#{project.id}/fork", admin), namespace: user2.username + + expect(response).to have_http_status(201) + expect(json_response['owner']['id']).to eq(user2.id) + end + + it 'fails if trying to fork to another user when not admin' do + post api("/projects/#{project.id}/fork", user2), namespace: admin.namespace.id + + expect(response).to have_http_status(404) + end + + it 'fails if trying to fork to non-existent namespace' do + post api("/projects/#{project.id}/fork", user2), namespace: 42424242 + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Target Namespace Not Found') + end + + it 'forks to owned group' do + post api("/projects/#{project.id}/fork", user2), namespace: group2.name + + expect(response).to have_http_status(201) + expect(json_response['namespace']['name']).to eq(group2.name) + end + + it 'fails to fork to not owned group' do + post api("/projects/#{project.id}/fork", user2), namespace: group.name + + expect(response).to have_http_status(404) + end + + it 'forks to not owned group when admin' do + post api("/projects/#{project.id}/fork", admin), namespace: group.name + + expect(response).to have_http_status(201) + expect(json_response['namespace']['name']).to eq(group.name) + end + end + + context 'when unauthenticated' do + it 'returns authentication error' do + post api("/projects/#{project.id}/fork") + + expect(response).to have_http_status(401) + expect(json_response['message']).to eq('401 Unauthorized') + end + end + end + + describe 'POST /projects/:id/housekeeping' do + let(:housekeeping) { Projects::HousekeepingService.new(project) } + + before do + allow(Projects::HousekeepingService).to receive(:new).with(project).and_return(housekeeping) + end + + context 'when authenticated as owner' do + it 'starts the housekeeping process' do + expect(housekeeping).to receive(:execute).once + + post api("/projects/#{project.id}/housekeeping", user) + + expect(response).to have_http_status(201) + end + + context 'when housekeeping lease is taken' do + it 'returns conflict' do + expect(housekeeping).to receive(:execute).once.and_raise(Projects::HousekeepingService::LeaseTaken) + + post api("/projects/#{project.id}/housekeeping", user) + + expect(response).to have_http_status(409) + expect(json_response['message']).to match(/Somebody already triggered housekeeping for this project/) + end + end + end + + context 'when authenticated as developer' do + before do + project_member2 + end + + it 'returns forbidden error' do + post api("/projects/#{project.id}/housekeeping", user3) + + expect(response).to have_http_status(403) + end + end + + context 'when unauthenticated' do + it 'returns authentication error' do + post api("/projects/#{project.id}/housekeeping") + + expect(response).to have_http_status(401) + end + end + end end diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index c61208e395c..7652606a491 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -19,10 +19,10 @@ describe API::Repositories, api: true do get api(route, current_user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array first_commit = json_response.first - - expect(json_response).to be_an Array expect(first_commit['name']).to eq('bar') expect(first_commit['type']).to eq('tree') expect(first_commit['mode']).to eq('040000') @@ -49,6 +49,7 @@ describe API::Repositories, api: true do expect(response.status).to eq(200) expect(json_response).to be_an Array + expect(response).to include_pagination_headers expect(json_response[4]['name']).to eq('html') expect(json_response[4]['path']).to eq('files/html') expect(json_response[4]['type']).to eq('tree') @@ -380,10 +381,10 @@ describe API::Repositories, api: true do get api(route, current_user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array first_contributor = json_response.first - expect(first_contributor['email']).to eq('tiagonbotelho@hotmail.com') expect(first_contributor['name']).to eq('tiagonbotelho') expect(first_contributor['commits']).to eq(1) diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb new file mode 100644 index 00000000000..e83202e4196 --- /dev/null +++ b/spec/requests/api/runner_spec.rb @@ -0,0 +1,151 @@ +require 'spec_helper' + +describe API::Runner do + include ApiHelpers + include StubGitlabCalls + + let(:registration_token) { 'abcdefg123456' } + + before do + stub_gitlab_calls + stub_application_setting(runners_registration_token: registration_token) + end + + describe '/api/v4/runners' do + describe 'POST /api/v4/runners' do + context 'when no token is provided' do + it 'returns 400 error' do + post api('/runners') + expect(response).to have_http_status 400 + end + end + + context 'when invalid token is provided' do + it 'returns 403 error' do + post api('/runners'), token: 'invalid' + expect(response).to have_http_status 403 + end + end + + context 'when valid token is provided' do + it 'creates runner with default values' do + post api('/runners'), token: registration_token + + runner = Ci::Runner.first + + expect(response).to have_http_status 201 + expect(json_response['id']).to eq(runner.id) + expect(json_response['token']).to eq(runner.token) + expect(runner.run_untagged).to be true + end + + context 'when project token is used' do + let(:project) { create(:empty_project) } + + it 'creates runner' do + post api('/runners'), token: project.runners_token + + expect(response).to have_http_status 201 + expect(project.runners.size).to eq(1) + end + end + end + + context 'when runner description is provided' do + it 'creates runner' do + post api('/runners'), token: registration_token, + description: 'server.hostname' + + expect(response).to have_http_status 201 + expect(Ci::Runner.first.description).to eq('server.hostname') + end + end + + context 'when runner tags are provided' do + it 'creates runner' do + post api('/runners'), token: registration_token, + tag_list: 'tag1, tag2' + + expect(response).to have_http_status 201 + expect(Ci::Runner.first.tag_list.sort).to eq(%w(tag1 tag2)) + end + end + + context 'when option for running untagged jobs is provided' do + context 'when tags are provided' do + it 'creates runner' do + post api('/runners'), token: registration_token, + run_untagged: false, + tag_list: ['tag'] + + expect(response).to have_http_status 201 + expect(Ci::Runner.first.run_untagged).to be false + expect(Ci::Runner.first.tag_list.sort).to eq(['tag']) + end + end + + context 'when tags are not provided' do + it 'returns 404 error' do + post api('/runners'), token: registration_token, + run_untagged: false + + expect(response).to have_http_status 404 + end + end + end + + context 'when option for locking Runner is provided' do + it 'creates runner' do + post api('/runners'), token: registration_token, + locked: true + + expect(response).to have_http_status 201 + expect(Ci::Runner.first.locked).to be true + end + end + + %w(name version revision platform architecture).each do |param| + context "when info parameter '#{param}' info is present" do + let(:value) { "#{param}_value" } + + it %q(updates provided Runner's parameter) do + post api('/runners'), token: registration_token, + info: { param => value } + + expect(response).to have_http_status 201 + expect(Ci::Runner.first.read_attribute(param.to_sym)).to eq(value) + end + end + end + end + + describe 'DELETE /api/v4/runners' do + context 'when no token is provided' do + it 'returns 400 error' do + delete api('/runners') + + expect(response).to have_http_status 400 + end + end + + context 'when invalid token is provided' do + it 'returns 403 error' do + delete api('/runners'), token: 'invalid' + + expect(response).to have_http_status 403 + end + end + + context 'when valid token is provided' do + let(:runner) { create(:ci_runner) } + + it 'deletes Runner' do + delete api('/runners'), token: runner.token + + expect(response).to have_http_status 204 + expect(Ci::Runner.count).to eq(0) + end + end + end + end +end diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb index f2d81a28cb8..8a82543a830 100644 --- a/spec/requests/api/runners_spec.rb +++ b/spec/requests/api/runners_spec.rb @@ -37,18 +37,20 @@ describe API::Runners, api: true do context 'authorized user' do it 'returns user available runners' do get api('/runners', user) - shared = json_response.any?{ |r| r['is_shared'] } + shared = json_response.any?{ |r| r['is_shared'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(shared).to be_falsey end it 'filters runners by scope' do get api('/runners?scope=active', user) - shared = json_response.any?{ |r| r['is_shared'] } + shared = json_response.any?{ |r| r['is_shared'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(shared).to be_falsey end @@ -73,9 +75,10 @@ describe API::Runners, api: true do context 'with admin privileges' do it 'returns all runners' do get api('/runners/all', admin) - shared = json_response.any?{ |r| r['is_shared'] } + shared = json_response.any?{ |r| r['is_shared'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(shared).to be_truthy end @@ -91,9 +94,10 @@ describe API::Runners, api: true do it 'filters runners by scope' do get api('/runners/all?scope=specific', admin) - shared = json_response.any?{ |r| r['is_shared'] } + shared = json_response.any?{ |r| r['is_shared'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(shared).to be_falsey end @@ -183,6 +187,7 @@ describe API::Runners, api: true do it 'updates runner' do description = shared_runner.description active = shared_runner.active + runner_queue_value = shared_runner.ensure_runner_queue_value update_runner(shared_runner.id, admin, description: "#{description}_updated", active: !active, @@ -197,18 +202,24 @@ describe API::Runners, api: true do expect(shared_runner.tag_list).to include('ruby2.1', 'pgsql', 'mysql') expect(shared_runner.run_untagged?).to be(false) expect(shared_runner.locked?).to be(true) + expect(shared_runner.ensure_runner_queue_value) + .not_to eq(runner_queue_value) end end context 'when runner is not shared' do it 'updates runner' do description = specific_runner.description + runner_queue_value = specific_runner.ensure_runner_queue_value + update_runner(specific_runner.id, admin, description: 'test') specific_runner.reload expect(response).to have_http_status(200) expect(specific_runner.description).to eq('test') expect(specific_runner.description).not_to eq(description) + expect(specific_runner.ensure_runner_queue_value) + .not_to eq(runner_queue_value) end end @@ -266,8 +277,9 @@ describe API::Runners, api: true do it 'deletes runner' do expect do delete api("/runners/#{shared_runner.id}", admin) + + expect(response).to have_http_status(204) end.to change{ Ci::Runner.shared.count }.by(-1) - expect(response).to have_http_status(200) end end @@ -275,15 +287,17 @@ describe API::Runners, api: true do it 'deletes unused runner' do expect do delete api("/runners/#{unused_specific_runner.id}", admin) + + expect(response).to have_http_status(204) end.to change{ Ci::Runner.specific.count }.by(-1) - expect(response).to have_http_status(200) end it 'deletes used runner' do expect do delete api("/runners/#{specific_runner.id}", admin) + + expect(response).to have_http_status(204) end.to change{ Ci::Runner.specific.count }.by(-1) - expect(response).to have_http_status(200) end end @@ -316,8 +330,9 @@ describe API::Runners, api: true do it 'deletes runner for one owned project' do expect do delete api("/runners/#{specific_runner.id}", user) + + expect(response).to have_http_status(204) end.to change{ Ci::Runner.specific.count }.by(-1) - expect(response).to have_http_status(200) end end end @@ -335,9 +350,10 @@ describe API::Runners, api: true do context 'authorized user with master privileges' do it "returns project's runners" do get api("/projects/#{project.id}/runners", user) - shared = json_response.any?{ |r| r['is_shared'] } + shared = json_response.any?{ |r| r['is_shared'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(shared).to be_truthy end @@ -445,8 +461,9 @@ describe API::Runners, api: true do it "disables project's runner" do expect do delete api("/projects/#{project.id}/runners/#{two_projects_runner.id}", user) + + expect(response).to have_http_status(204) end.to change{ project.runners.count }.by(-1) - expect(response).to have_http_status(200) end end diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb index 776dc655650..fd334934ca5 100644 --- a/spec/requests/api/services_spec.rb +++ b/spec/requests/api/services_spec.rb @@ -55,7 +55,7 @@ describe API::Services, api: true do it "deletes #{service}" do delete api("/projects/#{project.id}/services/#{dashed_service}", user) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) project.send(service_method).reload expect(project.send(service_method).activated?).to be_falsey end diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index 91e3c333a02..11b4b718e2c 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -18,6 +18,9 @@ describe API::Settings, 'Settings', api: true do expect(json_response['koding_url']).to be_nil expect(json_response['plantuml_enabled']).to be_falsey expect(json_response['plantuml_url']).to be_nil + expect(json_response['default_project_visibility']).to be_a String + expect(json_response['default_snippet_visibility']).to be_a String + expect(json_response['default_group_visibility']).to be_a String end end @@ -30,8 +33,16 @@ describe API::Settings, 'Settings', api: true do it "updates application settings" do put api("/application/settings", admin), - default_projects_limit: 3, signin_enabled: false, repository_storage: 'custom', koding_enabled: true, koding_url: 'http://koding.example.com', - plantuml_enabled: true, plantuml_url: 'http://plantuml.example.com' + default_projects_limit: 3, + signin_enabled: false, + repository_storage: 'custom', + koding_enabled: true, + koding_url: 'http://koding.example.com', + plantuml_enabled: true, + plantuml_url: 'http://plantuml.example.com', + default_snippet_visibility: 'internal', + restricted_visibility_levels: ['public'], + default_artifacts_expire_in: '2 days' expect(response).to have_http_status(200) expect(json_response['default_projects_limit']).to eq(3) expect(json_response['signin_enabled']).to be_falsey @@ -41,6 +52,9 @@ describe API::Settings, 'Settings', api: true do expect(json_response['koding_url']).to eq('http://koding.example.com') expect(json_response['plantuml_enabled']).to be_truthy expect(json_response['plantuml_url']).to eq('http://plantuml.example.com') + expect(json_response['default_snippet_visibility']).to eq('internal') + expect(json_response['restricted_visibility_levels']).to eq(['public']) + expect(json_response['default_artifacts_expire_in']).to eq('2 days') end end diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb index 6b9a739b439..5d75b47b3cd 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -13,6 +13,8 @@ describe API::Snippets, api: true do get api("/snippets/", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly( public_snippet.id, internal_snippet.id, @@ -25,7 +27,10 @@ describe API::Snippets, api: true do create(:personal_snippet, :private) get api("/snippets/", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array expect(json_response.size).to eq(0) end end @@ -43,6 +48,8 @@ describe API::Snippets, api: true do get api("/snippets/public", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly( public_snippet.id, public_snippet_other.id) @@ -67,7 +74,7 @@ describe API::Snippets, api: true do end it 'returns 404 for invalid snippet id' do - delete api("/snippets/1234", user) + get api("/snippets/1234/raw", user) expect(response).to have_http_status(404) expect(json_response['message']).to eq('404 Snippet Not Found') @@ -80,7 +87,7 @@ describe API::Snippets, api: true do title: 'Test Title', file_name: 'test.rb', content: 'puts "hello world"', - visibility_level: Snippet::PUBLIC + visibility: 'public' } end @@ -113,20 +120,22 @@ describe API::Snippets, api: true do context 'when the snippet is private' do it 'creates the snippet' do - expect { create_snippet(visibility_level: Snippet::PRIVATE) }. + expect { create_snippet(visibility: 'private') }. to change { Snippet.count }.by(1) end end context 'when the snippet is public' do it 'rejects the shippet' do - expect { create_snippet(visibility_level: Snippet::PUBLIC) }. + expect { create_snippet(visibility: 'public') }. not_to change { Snippet.count } + expect(response).to have_http_status(400) + expect(json_response['message']).to eq({ "error" => "Spam detected" }) end it 'creates a spam log' do - expect { create_snippet(visibility_level: Snippet::PUBLIC) }. + expect { create_snippet(visibility: 'public') }. to change { SpamLog.count }.by(1) end end @@ -134,16 +143,20 @@ describe API::Snippets, api: true do end describe 'PUT /snippets/:id' do + let(:visibility_level) { Snippet::PUBLIC } let(:other_user) { create(:user) } - let(:public_snippet) { create(:personal_snippet, :public, author: user) } + let(:snippet) do + create(:personal_snippet, author: user, visibility_level: visibility_level) + end + it 'updates snippet' do new_content = 'New content' - put api("/snippets/#{public_snippet.id}", user), content: new_content + put api("/snippets/#{snippet.id}", user), content: new_content expect(response).to have_http_status(200) - public_snippet.reload - expect(public_snippet.content).to eq(new_content) + snippet.reload + expect(snippet.content).to eq(new_content) end it 'returns 404 for invalid snippet id' do @@ -154,7 +167,7 @@ describe API::Snippets, api: true do end it "returns 404 for another user's snippet" do - put api("/snippets/#{public_snippet.id}", other_user), title: 'fubar' + put api("/snippets/#{snippet.id}", other_user), title: 'fubar' expect(response).to have_http_status(404) expect(json_response['message']).to eq('404 Snippet Not Found') @@ -165,6 +178,56 @@ describe API::Snippets, api: true do expect(response).to have_http_status(400) end + + context 'when the snippet is spam' do + def update_snippet(snippet_params = {}) + put api("/snippets/#{snippet.id}", user), snippet_params + end + + before do + allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) + end + + context 'when the snippet is private' do + let(:visibility_level) { Snippet::PRIVATE } + + it 'updates the snippet' do + expect { update_snippet(title: 'Foo') }. + to change { snippet.reload.title }.to('Foo') + end + end + + context 'when the snippet is public' do + let(:visibility_level) { Snippet::PUBLIC } + + it 'rejects the shippet' do + expect { update_snippet(title: 'Foo') }. + not_to change { snippet.reload.title } + + expect(response).to have_http_status(400) + expect(json_response['message']).to eq({ "error" => "Spam detected" }) + end + + it 'creates a spam log' do + expect { update_snippet(title: 'Foo') }. + to change { SpamLog.count }.by(1) + end + end + + context 'when a private snippet is made public' do + let(:visibility_level) { Snippet::PRIVATE } + + it 'rejects the snippet' do + expect { update_snippet(title: 'Foo', visibility: 'public') }. + not_to change { snippet.reload.title } + end + + it 'creates a spam log' do + expect { update_snippet(title: 'Foo', visibility: 'public') }. + to change { SpamLog.count }.by(1) + end + end + end end describe 'DELETE /snippets/:id' do diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb index b3e5afdadb1..d1e10f12657 100644 --- a/spec/requests/api/system_hooks_spec.rb +++ b/spec/requests/api/system_hooks_spec.rb @@ -31,6 +31,7 @@ describe API::SystemHooks, api: true do get api("/hooks", admin) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['url']).to eq(hook.url) expect(json_response.first['push_events']).to be true @@ -90,6 +91,8 @@ describe API::SystemHooks, api: true do it "deletes a hook" do expect do delete api("/hooks/#{hook.id}", admin) + + expect(response).to have_http_status(204) end.to change { SystemHook.count }.by(-1) end diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb index 898d2b27e5c..b132d033a61 100644 --- a/spec/requests/api/tags_spec.rb +++ b/spec/requests/api/tags_spec.rb @@ -20,10 +20,9 @@ describe API::Tags, api: true do get api("/projects/#{project.id}/repository/tags", current_user) expect(response).to have_http_status(200) - - first_tag = json_response.first - - expect(first_tag['name']).to eq(tag_name) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(tag_name) end end @@ -43,7 +42,9 @@ describe API::Tags, api: true do context 'without releases' do it "returns an array of project tags" do get api("/projects/#{project.id}/repository/tags", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['name']).to eq(tag_name) end @@ -59,6 +60,7 @@ describe API::Tags, api: true do get api("/projects/#{project.id}/repository/tags", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['name']).to eq(tag_name) expect(json_response.first['message']).to eq('Version 1.1.0') @@ -135,8 +137,8 @@ describe API::Tags, api: true do context 'delete tag' do it 'deletes an existing tag' do delete api("/projects/#{project.id}/repository/tags/#{tag_name}", user) - expect(response).to have_http_status(200) - expect(json_response['tag_name']).to eq(tag_name) + + expect(response).to have_http_status(204) end it 'raises 404 if the tag does not exist' do diff --git a/spec/requests/api/templates_spec.rb b/spec/requests/api/templates_spec.rb index d32ba60fc4c..2c83e119065 100644 --- a/spec/requests/api/templates_spec.rb +++ b/spec/requests/api/templates_spec.rb @@ -3,51 +3,53 @@ require 'spec_helper' describe API::Templates, api: true do include ApiHelpers - shared_examples_for 'the Template Entity' do |path| - before { get api(path) } + context 'the Template Entity' do + before { get api('/templates/gitignores/Ruby') } it { expect(json_response['name']).to eq('Ruby') } it { expect(json_response['content']).to include('*.gem') } end - - shared_examples_for 'the TemplateList Entity' do |path| - before { get api(path) } + + context 'the TemplateList Entity' do + before { get api('/templates/gitignores') } it { expect(json_response.first['name']).not_to be_nil } it { expect(json_response.first['content']).to be_nil } end - shared_examples_for 'requesting gitignores' do |path| + context 'requesting gitignores' do it 'returns a list of available gitignore templates' do - get api(path) + get api('/templates/gitignores') expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to be > 15 end end - shared_examples_for 'requesting gitlab-ci-ymls' do |path| + context 'requesting gitlab-ci-ymls' do it 'returns a list of available gitlab_ci_ymls' do - get api(path) + get api('/templates/gitlab_ci_ymls') expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['name']).not_to be_nil end end - shared_examples_for 'requesting gitlab-ci-yml for Ruby' do |path| + context 'requesting gitlab-ci-yml for Ruby' do it 'adds a disclaimer on the top' do - get api(path) + get api('/templates/gitlab_ci_ymls/Ruby') expect(response).to have_http_status(200) expect(json_response['content']).to start_with("# This file is a template,") end end - shared_examples_for 'the License Template Entity' do |path| - before { get api(path) } + context 'the License Template Entity' do + before { get api('/templates/licenses/mit') } it 'returns a license template' do expect(json_response['key']).to eq('mit') @@ -56,30 +58,32 @@ describe API::Templates, api: true do expect(json_response['popular']).to be true expect(json_response['html_url']).to eq('http://choosealicense.com/licenses/mit/') expect(json_response['source_url']).to eq('https://opensource.org/licenses/MIT') - expect(json_response['description']).to include('A permissive license that is short and to the point.') + expect(json_response['description']).to include('A short and simple permissive license with conditions') expect(json_response['conditions']).to eq(%w[include-copyright]) expect(json_response['permissions']).to eq(%w[commercial-use modifications distribution private-use]) expect(json_response['limitations']).to eq(%w[no-liability]) - expect(json_response['content']).to include('The MIT License (MIT)') + expect(json_response['content']).to include('MIT License') end end - shared_examples_for 'GET licenses' do |path| + context 'GET templates/licenses' do it 'returns a list of available license templates' do - get api(path) + get api('/templates/licenses') expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.size).to eq(15) + expect(json_response.size).to eq(12) expect(json_response.map { |l| l['key'] }).to include('agpl-3.0') end describe 'the popular parameter' do context 'with popular=1' do it 'returns a list of available popular license templates' do - get api("#{path}?popular=1") + get api('/templates/licenses?popular=1') expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(3) expect(json_response.map { |l| l['key'] }).to include('apache-2.0') @@ -88,17 +92,17 @@ describe API::Templates, api: true do end end - shared_examples_for 'GET licenses/:name' do |path| + context 'GET templates/licenses/:name' do context 'with :project and :fullname given' do before do - get api("#{path}/#{license_type}?project=My+Awesome+Project&fullname=Anton+#{license_type.upcase}") + get api("/templates/licenses/#{license_type}?project=My+Awesome+Project&fullname=Anton+#{license_type.upcase}") end context 'for the mit license' do let(:license_type) { 'mit' } it 'returns the license text' do - expect(json_response['content']).to include('The MIT License (MIT)') + expect(json_response['content']).to include('MIT License') end it 'replaces placeholder values' do @@ -178,26 +182,4 @@ describe API::Templates, api: true do end end end - - describe 'with /templates namespace' do - it_behaves_like 'the Template Entity', '/templates/gitignores/Ruby' - it_behaves_like 'the TemplateList Entity', '/templates/gitignores' - it_behaves_like 'requesting gitignores', '/templates/gitignores' - it_behaves_like 'requesting gitlab-ci-ymls', '/templates/gitlab_ci_ymls' - it_behaves_like 'requesting gitlab-ci-yml for Ruby', '/templates/gitlab_ci_ymls/Ruby' - it_behaves_like 'the License Template Entity', '/templates/licenses/mit' - it_behaves_like 'GET licenses', '/templates/licenses' - it_behaves_like 'GET licenses/:name', '/templates/licenses' - end - - describe 'without /templates namespace' do - it_behaves_like 'the Template Entity', '/gitignores/Ruby' - it_behaves_like 'the TemplateList Entity', '/gitignores' - it_behaves_like 'requesting gitignores', '/gitignores' - it_behaves_like 'requesting gitlab-ci-ymls', '/gitlab_ci_ymls' - it_behaves_like 'requesting gitlab-ci-yml for Ruby', '/gitlab_ci_ymls/Ruby' - it_behaves_like 'the License Template Entity', '/licenses/mit' - it_behaves_like 'GET licenses', '/licenses' - it_behaves_like 'GET licenses/:name', '/licenses' - end end diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb index 56dc017ce54..1e401935662 100644 --- a/spec/requests/api/todos_spec.rb +++ b/spec/requests/api/todos_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe API::Todos, api: true do include ApiHelpers - let(:project_1) { create(:empty_project) } + let(:project_1) { create(:empty_project, :test_repo) } let(:project_2) { create(:empty_project) } let(:author_1) { create(:user) } let(:author_2) { create(:user) } @@ -11,7 +11,7 @@ describe API::Todos, api: true do let(:merge_request) { create(:merge_request, source_project: project_1) } let!(:pending_1) { create(:todo, :mentioned, project: project_1, author: author_1, user: john_doe) } let!(:pending_2) { create(:todo, project: project_2, author: author_2, user: john_doe) } - let!(:pending_3) { create(:todo, project: project_1, author: author_2, user: john_doe) } + let!(:pending_3) { create(:on_commit_todo, project: project_1, author: author_2, user: john_doe) } let!(:done) { create(:todo, :done, project: project_1, author: author_1, user: john_doe) } before do @@ -33,6 +33,7 @@ describe API::Todos, api: true do get api('/todos', john_doe) expect(response.status).to eq(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) expect(json_response[0]['id']).to eq(pending_3.id) @@ -52,6 +53,7 @@ describe API::Todos, api: true do get api('/todos', john_doe), { author_id: author_2.id } expect(response.status).to eq(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) end @@ -64,6 +66,7 @@ describe API::Todos, api: true do get api('/todos', john_doe), { type: 'MergeRequest' } expect(response.status).to eq(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) end @@ -74,6 +77,7 @@ describe API::Todos, api: true do get api('/todos', john_doe), { state: 'done' } expect(response.status).to eq(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) end @@ -84,6 +88,7 @@ describe API::Todos, api: true do get api('/todos', john_doe), { project_id: project_2.id } expect(response.status).to eq(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) end @@ -94,6 +99,7 @@ describe API::Todos, api: true do get api('/todos', john_doe), { action: 'mentioned' } expect(response.status).to eq(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) end @@ -101,46 +107,47 @@ describe API::Todos, api: true do end end - describe 'DELETE /todos/:id' do + describe 'POST /todos/:id/mark_as_done' do context 'when unauthenticated' do it 'returns authentication error' do - delete api("/todos/#{pending_1.id}") + post api("/todos/#{pending_1.id}/mark_as_done") - expect(response.status).to eq(401) + expect(response).to have_http_status(401) end end context 'when authenticated' do it 'marks a todo as done' do - delete api("/todos/#{pending_1.id}", john_doe) + post api("/todos/#{pending_1.id}/mark_as_done", john_doe) - expect(response.status).to eq(200) + expect(response).to have_http_status(201) + expect(json_response['id']).to eq(pending_1.id) + expect(json_response['state']).to eq('done') expect(pending_1.reload).to be_done end it 'updates todos cache' do expect_any_instance_of(User).to receive(:update_todos_count_cache).and_call_original - delete api("/todos/#{pending_1.id}", john_doe) + post api("/todos/#{pending_1.id}/mark_as_done", john_doe) end end end - describe 'DELETE /todos' do + describe 'POST /mark_as_done' do context 'when unauthenticated' do it 'returns authentication error' do - delete api('/todos') + post api('/todos/mark_as_done') - expect(response.status).to eq(401) + expect(response).to have_http_status(401) end end context 'when authenticated' do it 'marks all todos as done' do - delete api('/todos', john_doe) + post api('/todos/mark_as_done', john_doe) - expect(response.status).to eq(200) - expect(response.body).to eq('3') + expect(response).to have_http_status(204) expect(pending_1.reload).to be_done expect(pending_2.reload).to be_done expect(pending_3.reload).to be_done @@ -149,7 +156,7 @@ describe API::Todos, api: true do it 'updates todos cache' do expect_any_instance_of(User).to receive(:update_todos_count_cache).and_call_original - delete api("/todos", john_doe) + post api("/todos/mark_as_done", john_doe) end end end diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb index 84104aa66ee..153e2791cbe 100644 --- a/spec/requests/api/triggers_spec.rb +++ b/spec/requests/api/triggers_spec.rb @@ -100,6 +100,7 @@ describe API::Triggers do get api("/projects/#{project.id}/triggers", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_a(Array) expect(json_response[0]).to have_key('token') end @@ -189,8 +190,9 @@ describe API::Triggers do it 'deletes trigger' do expect do delete api("/projects/#{project.id}/triggers/#{trigger.token}", user) + + expect(response).to have_http_status(204) end.to change{project.triggers.count}.by(-1) - expect(response).to have_http_status(200) end it 'responds with 404 Not Found if requesting non-existing trigger' do diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 8692f9da976..e5e4c84755f 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -40,7 +40,9 @@ describe API::Users, api: true do it "returns an array of users" do get api("/users", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array username = user.username expect(json_response.detect do |user| @@ -55,13 +57,16 @@ describe API::Users, api: true do get api("/users?blocked=true", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response).to all(include('state' => /(blocked|ldap_blocked)/)) end it "returns one user" do get api("/users?username=#{omniauth_user.username}", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['username']).to eq(omniauth_user.username) end @@ -70,7 +75,9 @@ describe API::Users, api: true do context "when admin" do it "returns an array of users" do get api("/users", admin) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first.keys).to include 'email' expect(json_response.first.keys).to include 'organization' @@ -87,6 +94,7 @@ describe API::Users, api: true do get api("/users?external=true", admin) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response).to all(include('external' => true)) end @@ -190,6 +198,18 @@ describe API::Users, api: true do expect(new_user.external).to be_truthy end + it "creates user with reset password" do + post api('/users', admin), attributes_for(:user, reset_password: true).except(:password) + + expect(response).to have_http_status(201) + + user_id = json_response['id'] + new_user = User.find(user_id) + + expect(new_user).not_to eq(nil) + expect(new_user.recently_sent_password_reset?).to eq(true) + end + it "does not create user with invalid email" do post api('/users', admin), email: 'invalid email', @@ -495,8 +515,11 @@ describe API::Users, api: true do it 'returns array of ssh keys' do user.keys << key user.save + get api("/users/#{user.id}/keys", admin) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['title']).to eq(key.title) end @@ -517,10 +540,12 @@ describe API::Users, api: true do it 'deletes existing key' do user.keys << key user.save + expect do delete api("/users/#{user.id}/keys/#{key.id}", admin) + + expect(response).to have_http_status(204) end.to change { user.keys.count }.by(-1) - expect(response).to have_http_status(200) end it 'returns 404 error if user not found' do @@ -583,8 +608,11 @@ describe API::Users, api: true do it 'returns array of emails' do user.emails << email user.save + get api("/users/#{user.id}/emails", admin) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['email']).to eq(email.email) end @@ -611,10 +639,12 @@ describe API::Users, api: true do it 'deletes existing email' do user.emails << email user.save + expect do delete api("/users/#{user.id}/emails/#{email.id}", admin) + + expect(response).to have_http_status(204) end.to change { user.emails.count }.by(-1) - expect(response).to have_http_status(200) end it 'returns 404 error if user not found' do @@ -645,10 +675,10 @@ describe API::Users, api: true do it "deletes user" do delete api("/users/#{user.id}", admin) - expect(response).to have_http_status(200) + + expect(response).to have_http_status(204) expect { User.find(user.id) }.to raise_error ActiveRecord::RecordNotFound expect { Namespace.find(namespace.id) }.to raise_error ActiveRecord::RecordNotFound - expect(json_response['email']).to eq(user.email) end it "does not delete for unauthenticated user" do @@ -762,8 +792,11 @@ describe API::Users, api: true do it "returns array of ssh keys" do user.keys << key user.save + get api("/user/keys", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first["title"]).to eq(key.title) end @@ -840,10 +873,12 @@ describe API::Users, api: true do it "deletes existed key" do user.keys << key user.save + expect do delete api("/user/keys/#{key.id}", user) + + expect(response).to have_http_status(204) end.to change{user.keys.count}.by(-1) - expect(response).to have_http_status(200) end it "returns 404 if key ID not found" do @@ -879,8 +914,11 @@ describe API::Users, api: true do it "returns array of emails" do user.emails << email user.save + get api("/user/emails", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first["email"]).to eq(email.email) end @@ -944,10 +982,12 @@ describe API::Users, api: true do it "deletes existed email" do user.emails << email user.save + expect do delete api("/user/emails/#{email.id}", user) + + expect(response).to have_http_status(204) end.to change{user.emails.count}.by(-1) - expect(response).to have_http_status(200) end it "returns 404 if email ID not found" do @@ -971,69 +1011,69 @@ describe API::Users, api: true do end end - describe 'PUT /users/:id/block' do + describe 'POST /users/:id/block' do before { admin } it 'blocks existing user' do - put api("/users/#{user.id}/block", admin) - expect(response).to have_http_status(200) + post api("/users/#{user.id}/block", admin) + expect(response).to have_http_status(201) expect(user.reload.state).to eq('blocked') end it 'does not re-block ldap blocked users' do - put api("/users/#{ldap_blocked_user.id}/block", admin) + post api("/users/#{ldap_blocked_user.id}/block", admin) expect(response).to have_http_status(403) expect(ldap_blocked_user.reload.state).to eq('ldap_blocked') end it 'does not be available for non admin users' do - put api("/users/#{user.id}/block", user) + post api("/users/#{user.id}/block", user) expect(response).to have_http_status(403) expect(user.reload.state).to eq('active') end it 'returns a 404 error if user id not found' do - put api('/users/9999/block', admin) + post api('/users/9999/block', admin) expect(response).to have_http_status(404) expect(json_response['message']).to eq('404 User Not Found') end end - describe 'PUT /users/:id/unblock' do + describe 'POST /users/:id/unblock' do let(:blocked_user) { create(:user, state: 'blocked') } before { admin } it 'unblocks existing user' do - put api("/users/#{user.id}/unblock", admin) - expect(response).to have_http_status(200) + post api("/users/#{user.id}/unblock", admin) + expect(response).to have_http_status(201) expect(user.reload.state).to eq('active') end it 'unblocks a blocked user' do - put api("/users/#{blocked_user.id}/unblock", admin) - expect(response).to have_http_status(200) + post api("/users/#{blocked_user.id}/unblock", admin) + expect(response).to have_http_status(201) expect(blocked_user.reload.state).to eq('active') end it 'does not unblock ldap blocked users' do - put api("/users/#{ldap_blocked_user.id}/unblock", admin) + post api("/users/#{ldap_blocked_user.id}/unblock", admin) expect(response).to have_http_status(403) expect(ldap_blocked_user.reload.state).to eq('ldap_blocked') end it 'does not be available for non admin users' do - put api("/users/#{user.id}/unblock", user) + post api("/users/#{user.id}/unblock", user) expect(response).to have_http_status(403) expect(user.reload.state).to eq('active') end it 'returns a 404 error if user id not found' do - put api('/users/9999/block', admin) + post api('/users/9999/block', admin) expect(response).to have_http_status(404) expect(json_response['message']).to eq('404 User Not Found') end it "returns a 404 for invalid ID" do - put api("/users/ASDF/block", admin) + post api("/users/ASDF/block", admin) expect(response).to have_http_status(404) end @@ -1061,14 +1101,14 @@ describe API::Users, api: true do end context "as a user than can see the event's project" do - it_behaves_like 'a paginated resources' do - let(:request) { get api("/users/#{user.id}/events", user) } - end - context 'joined event' do it 'returns the "joined" event' do get api("/users/#{user.id}/events", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + comment_event = json_response.find { |e| e['action_name'] == 'commented on' } expect(comment_event['project_id'].to_i).to eq(project.id) diff --git a/spec/requests/api/v3/award_emoji_spec.rb b/spec/requests/api/v3/award_emoji_spec.rb new file mode 100644 index 00000000000..91145c8e72c --- /dev/null +++ b/spec/requests/api/v3/award_emoji_spec.rb @@ -0,0 +1,74 @@ +require 'spec_helper' + +describe API::V3::AwardEmoji, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let!(:project) { create(:empty_project) } + 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) } + let!(:note) { create(:note, project: project, noteable: issue) } + + before { project.team << [user, :master] } + + describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_id' do + context 'when the awardable is an Issue' do + it 'deletes the award' do + expect do + delete v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/#{award_emoji.id}", user) + + expect(response).to have_http_status(200) + end.to change { issue.award_emoji.count }.from(1).to(0) + end + + it 'returns a 404 error when the award emoji can not be found' do + delete v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/12345", user) + + expect(response).to have_http_status(404) + end + end + + context 'when the awardable is a Merge Request' do + it 'deletes the award' do + expect do + delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user) + + expect(response).to have_http_status(200) + end.to change { merge_request.award_emoji.count }.from(1).to(0) + end + + it 'returns a 404 error when note id not found' do + delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes/12345", user) + + expect(response).to have_http_status(404) + end + end + + context 'when the awardable is a Snippet' do + let(:snippet) { create(:project_snippet, :public, project: project) } + let!(:award) { create(:award_emoji, awardable: snippet, user: user) } + + it 'deletes the award' do + expect do + delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user) + + expect(response).to have_http_status(200) + end.to change { snippet.award_emoji.count }.from(1).to(0) + end + end + end + + describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_emoji_id' do + let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket', user: user) } + + it 'deletes the award' do + expect do + delete v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji/#{rocket.id}", user) + + expect(response).to have_http_status(200) + end.to change { note.award_emoji.count }.from(1).to(0) + end + end +end diff --git a/spec/requests/api/v3/boards_spec.rb b/spec/requests/api/v3/boards_spec.rb new file mode 100644 index 00000000000..eb95934f354 --- /dev/null +++ b/spec/requests/api/v3/boards_spec.rb @@ -0,0 +1,113 @@ +require 'spec_helper' + +describe API::V3::Boards, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:guest) { create(:user) } + let(:non_member) { create(:user) } + let!(:project) { create(:empty_project, :public, creator_id: user.id, namespace: user.namespace ) } + + let!(:dev_label) do + create(:label, title: 'Development', color: '#FFAABB', project: project) + end + + let!(:test_label) do + create(:label, title: 'Testing', color: '#FFAACC', project: project) + end + + let!(:dev_list) do + create(:list, label: dev_label, position: 1) + end + + let!(:test_list) do + create(:list, label: test_label, position: 2) + end + + let!(:board) do + create(:board, project: project, lists: [dev_list, test_list]) + end + + before do + project.team << [user, :reporter] + project.team << [guest, :guest] + end + + describe "GET /projects/:id/boards" do + let(:base_url) { "/projects/#{project.id}/boards" } + + context "when unauthenticated" do + it "returns authentication error" do + get v3_api(base_url) + + expect(response).to have_http_status(401) + end + end + + context "when authenticated" do + it "returns the project issue board" do + get v3_api(base_url, user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(board.id) + expect(json_response.first['lists']).to be_an Array + expect(json_response.first['lists'].length).to eq(2) + expect(json_response.first['lists'].last).to have_key('position') + end + end + end + + describe "GET /projects/:id/boards/:board_id/lists" do + let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" } + + it 'returns issue board lists' do + get v3_api(base_url, user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.first['label']['name']).to eq(dev_label.title) + end + + it 'returns 404 if board not found' do + get v3_api("/projects/#{project.id}/boards/22343/lists", user) + + expect(response).to have_http_status(404) + end + end + + describe "DELETE /projects/:id/board/lists/:list_id" do + let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" } + + it "rejects a non member from deleting a list" do + delete v3_api("#{base_url}/#{dev_list.id}", non_member) + + expect(response).to have_http_status(403) + end + + it "rejects a user with guest role from deleting a list" do + delete v3_api("#{base_url}/#{dev_list.id}", guest) + + expect(response).to have_http_status(403) + end + + it "returns 404 error if list id not found" do + delete v3_api("#{base_url}/44444", user) + + expect(response).to have_http_status(404) + end + + context "when the user is project owner" do + let(:owner) { create(:user) } + let(:project) { create(:empty_project, namespace: owner.namespace) } + + it "deletes the list if an admin requests it" do + delete v3_api("#{base_url}/#{dev_list.id}", owner) + + expect(response).to have_http_status(200) + end + end + end +end diff --git a/spec/requests/api/v3/branches_spec.rb b/spec/requests/api/v3/branches_spec.rb new file mode 100644 index 00000000000..e4cedf98e64 --- /dev/null +++ b/spec/requests/api/v3/branches_spec.rb @@ -0,0 +1,83 @@ +require 'spec_helper' +require 'mime/types' + +describe API::V3::Branches, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:user2) { create(:user) } + let!(:project) { create(:project, :repository, creator: user) } + let!(:master) { create(:project_member, :master, user: user, project: project) } + let!(:guest) { create(:project_member, :guest, user: user2, project: project) } + let!(:branch_name) { 'feature' } + let!(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master") } + + describe "GET /projects/:id/repository/branches" do + it "returns an array of project branches" do + project.repository.expire_all_method_caches + + get v3_api("/projects/#{project.id}/repository/branches", user), per_page: 100 + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + branch_names = json_response.map { |x| x['name'] } + expect(branch_names).to match_array(project.repository.branch_names) + end + end + + describe "DELETE /projects/:id/repository/branches/:branch" do + before do + allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true) + end + + it "removes branch" do + delete v3_api("/projects/#{project.id}/repository/branches/#{branch_name}", user) + + expect(response).to have_http_status(200) + expect(json_response['branch_name']).to eq(branch_name) + end + + it "removes a branch with dots in the branch name" do + delete v3_api("/projects/#{project.id}/repository/branches/with.1.2.3", user) + + expect(response).to have_http_status(200) + expect(json_response['branch_name']).to eq("with.1.2.3") + end + + it 'returns 404 if branch not exists' do + delete v3_api("/projects/#{project.id}/repository/branches/foobar", user) + expect(response).to have_http_status(404) + end + + it "removes protected branch" do + create(:protected_branch, project: project, name: branch_name) + delete v3_api("/projects/#{project.id}/repository/branches/#{branch_name}", user) + expect(response).to have_http_status(405) + expect(json_response['message']).to eq('Protected branch cant be removed') + end + + it "does not remove HEAD branch" do + delete v3_api("/projects/#{project.id}/repository/branches/master", user) + expect(response).to have_http_status(405) + expect(json_response['message']).to eq('Cannot remove HEAD branch') + end + end + + describe "DELETE /projects/:id/repository/merged_branches" do + before do + allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true) + end + + it 'returns 200' do + delete v3_api("/projects/#{project.id}/repository/merged_branches", user) + + expect(response).to have_http_status(200) + end + + it 'returns a 403 error if guest' do + delete v3_api("/projects/#{project.id}/repository/merged_branches", user2) + + expect(response).to have_http_status(403) + end + end +end diff --git a/spec/requests/api/v3/broadcast_messages_spec.rb b/spec/requests/api/v3/broadcast_messages_spec.rb new file mode 100644 index 00000000000..06556401a29 --- /dev/null +++ b/spec/requests/api/v3/broadcast_messages_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe API::V3::BroadcastMessages, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:admin) { create(:admin) } + + describe 'DELETE /broadcast_messages/:id' do + let!(:message) { create(:broadcast_message) } + + it 'returns a 401 for anonymous users' do + delete v3_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 v3_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 do + delete v3_api("/broadcast_messages/#{message.id}", admin) + + expect(response).to have_http_status(200) + end.to change { BroadcastMessage.count }.by(-1) + end + end +end diff --git a/spec/requests/api/v3/commits_spec.rb b/spec/requests/api/v3/commits_spec.rb new file mode 100644 index 00000000000..e298ef055e1 --- /dev/null +++ b/spec/requests/api/v3/commits_spec.rb @@ -0,0 +1,578 @@ +require 'spec_helper' +require 'mime/types' + +describe API::V3::Commits, api: true do + include ApiHelpers + let(:user) { create(:user) } + let(:user2) { create(:user) } + let!(:project) { create(:project, :repository, creator: user, namespace: user.namespace) } + let!(:master) { create(:project_member, :master, user: user, project: project) } + let!(:guest) { create(:project_member, :guest, user: user2, project: project) } + let!(:note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'a comment on a commit') } + let!(:another_note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'another comment on a commit') } + + before { project.team << [user, :reporter] } + + describe "List repository commits" do + context "authorized user" do + before { project.team << [user2, :reporter] } + + it "returns project commits" do + commit = project.repository.commit + get v3_api("/projects/#{project.id}/repository/commits", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['id']).to eq(commit.id) + expect(json_response.first['committer_name']).to eq(commit.committer_name) + expect(json_response.first['committer_email']).to eq(commit.committer_email) + end + end + + context "unauthorized user" do + it "does not return project commits" do + get v3_api("/projects/#{project.id}/repository/commits") + expect(response).to have_http_status(401) + end + end + + context "since optional parameter" do + it "returns project commits since provided parameter" do + commits = project.repository.commits("master") + since = commits.second.created_at + + get v3_api("/projects/#{project.id}/repository/commits?since=#{since.utc.iso8601}", user) + + expect(json_response.size).to eq 2 + expect(json_response.first["id"]).to eq(commits.first.id) + expect(json_response.second["id"]).to eq(commits.second.id) + end + end + + context "until optional parameter" do + it "returns project commits until provided parameter" do + commits = project.repository.commits("master") + before = commits.second.created_at + + get v3_api("/projects/#{project.id}/repository/commits?until=#{before.utc.iso8601}", user) + + if commits.size >= 20 + expect(json_response.size).to eq(20) + else + expect(json_response.size).to eq(commits.size - 1) + end + + expect(json_response.first["id"]).to eq(commits.second.id) + expect(json_response.second["id"]).to eq(commits.third.id) + end + end + + context "invalid xmlschema date parameters" do + it "returns an invalid parameter error message" do + get v3_api("/projects/#{project.id}/repository/commits?since=invalid-date", user) + + expect(response).to have_http_status(400) + expect(json_response['error']).to eq('since is invalid') + end + end + + context "path optional parameter" do + it "returns project commits matching provided path parameter" do + path = 'files/ruby/popen.rb' + + get v3_api("/projects/#{project.id}/repository/commits?path=#{path}", user) + + expect(json_response.size).to eq(3) + expect(json_response.first["id"]).to eq("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") + end + end + end + + describe "Create a commit with multiple files and actions" do + let!(:url) { "/projects/#{project.id}/repository/commits" } + + it 'returns a 403 unauthorized for user without permissions' do + post v3_api(url, user2) + + expect(response).to have_http_status(403) + end + + it 'returns a 400 bad request if no params are given' do + post v3_api(url, user) + + expect(response).to have_http_status(400) + end + + context :create do + let(:message) { 'Created file' } + let!(:invalid_c_params) do + { + branch_name: 'master', + commit_message: message, + actions: [ + { + action: 'create', + file_path: 'files/ruby/popen.rb', + content: 'puts 8' + } + ] + } + end + let!(:valid_c_params) do + { + branch_name: 'master', + commit_message: message, + actions: [ + { + action: 'create', + file_path: 'foo/bar/baz.txt', + content: 'puts 8' + } + ] + } + end + + it 'a new file in project repo' do + post v3_api(url, user), valid_c_params + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq(message) + expect(json_response['committer_name']).to eq(user.name) + expect(json_response['committer_email']).to eq(user.email) + end + + it 'returns a 400 bad request if file exists' do + post v3_api(url, user), invalid_c_params + + expect(response).to have_http_status(400) + end + + context 'with project path in URL' do + let(:url) { "/projects/#{project.full_path.gsub('/', '%2F')}/repository/commits" } + + it 'a new file in project repo' do + post v3_api(url, user), valid_c_params + + expect(response).to have_http_status(201) + end + end + end + + context :delete do + let(:message) { 'Deleted file' } + let!(:invalid_d_params) do + { + branch_name: 'markdown', + commit_message: message, + actions: [ + { + action: 'delete', + file_path: 'doc/api/projects.md' + } + ] + } + end + let!(:valid_d_params) do + { + branch_name: 'markdown', + commit_message: message, + actions: [ + { + action: 'delete', + file_path: 'doc/api/users.md' + } + ] + } + end + + it 'an existing file in project repo' do + post v3_api(url, user), valid_d_params + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq(message) + end + + it 'returns a 400 bad request if file does not exist' do + post v3_api(url, user), invalid_d_params + + expect(response).to have_http_status(400) + end + end + + context :move do + let(:message) { 'Moved file' } + let!(:invalid_m_params) do + { + branch_name: 'feature', + commit_message: message, + actions: [ + { + action: 'move', + file_path: 'CHANGELOG', + previous_path: 'VERSION', + content: '6.7.0.pre' + } + ] + } + end + let!(:valid_m_params) do + { + branch_name: 'feature', + commit_message: message, + actions: [ + { + action: 'move', + file_path: 'VERSION.txt', + previous_path: 'VERSION', + content: '6.7.0.pre' + } + ] + } + end + + it 'an existing file in project repo' do + post v3_api(url, user), valid_m_params + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq(message) + end + + it 'returns a 400 bad request if file does not exist' do + post v3_api(url, user), invalid_m_params + + expect(response).to have_http_status(400) + end + end + + context :update do + let(:message) { 'Updated file' } + let!(:invalid_u_params) do + { + branch_name: 'master', + commit_message: message, + actions: [ + { + action: 'update', + file_path: 'foo/bar.baz', + content: 'puts 8' + } + ] + } + end + let!(:valid_u_params) do + { + branch_name: 'master', + commit_message: message, + actions: [ + { + action: 'update', + file_path: 'files/ruby/popen.rb', + content: 'puts 8' + } + ] + } + end + + it 'an existing file in project repo' do + post v3_api(url, user), valid_u_params + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq(message) + end + + it 'returns a 400 bad request if file does not exist' do + post v3_api(url, user), invalid_u_params + + expect(response).to have_http_status(400) + end + end + + context "multiple operations" do + let(:message) { 'Multiple actions' } + let!(:invalid_mo_params) do + { + branch_name: 'master', + commit_message: message, + actions: [ + { + action: 'create', + file_path: 'files/ruby/popen.rb', + content: 'puts 8' + }, + { + action: 'delete', + file_path: 'doc/v3_api/projects.md' + }, + { + action: 'move', + file_path: 'CHANGELOG', + previous_path: 'VERSION', + content: '6.7.0.pre' + }, + { + action: 'update', + file_path: 'foo/bar.baz', + content: 'puts 8' + } + ] + } + end + let!(:valid_mo_params) do + { + branch_name: 'master', + commit_message: message, + actions: [ + { + action: 'create', + file_path: 'foo/bar/baz.txt', + content: 'puts 8' + }, + { + action: 'delete', + file_path: 'Gemfile.zip' + }, + { + action: 'move', + file_path: 'VERSION.txt', + previous_path: 'VERSION', + content: '6.7.0.pre' + }, + { + action: 'update', + file_path: 'files/ruby/popen.rb', + content: 'puts 8' + } + ] + } + end + + it 'are commited as one in project repo' do + post v3_api(url, user), valid_mo_params + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq(message) + end + + it 'return a 400 bad request if there are any issues' do + post v3_api(url, user), invalid_mo_params + + expect(response).to have_http_status(400) + end + end + end + + describe "Get a single commit" do + context "authorized user" do + it "returns a commit by sha" do + get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['id']).to eq(project.repository.commit.id) + expect(json_response['title']).to eq(project.repository.commit.title) + expect(json_response['stats']['additions']).to eq(project.repository.commit.stats.additions) + expect(json_response['stats']['deletions']).to eq(project.repository.commit.stats.deletions) + expect(json_response['stats']['total']).to eq(project.repository.commit.stats.total) + end + + it "returns a 404 error if not found" do + get v3_api("/projects/#{project.id}/repository/commits/invalid_sha", user) + expect(response).to have_http_status(404) + end + + it "returns nil for commit without CI" do + get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['status']).to be_nil + end + + it "returns status for CI" do + pipeline = project.ensure_pipeline('master', project.repository.commit.sha) + pipeline.update(status: 'success') + + get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['status']).to eq(pipeline.status) + end + + it "returns status for CI when pipeline is created" do + project.ensure_pipeline('master', project.repository.commit.sha) + + get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['status']).to eq("created") + end + end + + context "unauthorized user" do + it "does not return the selected commit" do + get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}") + expect(response).to have_http_status(401) + end + end + end + + describe "Get the diff of a commit" do + context "authorized user" do + before { project.team << [user2, :reporter] } + + it "returns the diff of the selected commit" do + get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/diff", user) + expect(response).to have_http_status(200) + + expect(json_response).to be_an Array + expect(json_response.length).to be >= 1 + expect(json_response.first.keys).to include "diff" + end + + it "returns a 404 error if invalid commit" do + get v3_api("/projects/#{project.id}/repository/commits/invalid_sha/diff", user) + expect(response).to have_http_status(404) + end + end + + context "unauthorized user" do + it "does not return the diff of the selected commit" do + get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/diff") + expect(response).to have_http_status(401) + end + end + end + + describe 'Get the comments of a commit' do + context 'authorized user' do + it 'returns merge_request comments' do + get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.first['note']).to eq('a comment on a commit') + expect(json_response.first['author']['id']).to eq(user.id) + end + + it 'returns a 404 error if merge_request_id not found' do + get v3_api("/projects/#{project.id}/repository/commits/1234ab/comments", user) + expect(response).to have_http_status(404) + end + end + + context 'unauthorized user' do + it 'does not return the diff of the selected commit' do + get v3_api("/projects/#{project.id}/repository/commits/1234ab/comments") + expect(response).to have_http_status(401) + end + end + end + + describe 'POST :id/repository/commits/:sha/cherry_pick' do + let(:master_pickable_commit) { project.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') } + + context 'authorized user' do + it 'cherry picks a commit' do + post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'master' + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq(master_pickable_commit.title) + expect(json_response['message']).to eq(master_pickable_commit.message) + expect(json_response['author_name']).to eq(master_pickable_commit.author_name) + expect(json_response['committer_name']).to eq(user.name) + end + + it 'returns 400 if commit is already included in the target branch' do + post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'markdown' + + expect(response).to have_http_status(400) + expect(json_response['message']).to eq('Sorry, we cannot cherry-pick this commit automatically. + A cherry-pick may have already been performed with this commit, or a more recent commit may have updated some of its content.') + end + + it 'returns 400 if you are not allowed to push to the target branch' do + project.team << [user2, :developer] + protected_branch = create(:protected_branch, project: project, name: 'feature') + + post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user2), branch: protected_branch.name + + expect(response).to have_http_status(400) + expect(json_response['message']).to eq('You are not allowed to push into this branch') + end + + it 'returns 400 for missing parameters' do + post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user) + + expect(response).to have_http_status(400) + expect(json_response['error']).to eq('branch is missing') + end + + it 'returns 404 if commit is not found' do + post v3_api("/projects/#{project.id}/repository/commits/abcd0123/cherry_pick", user), branch: 'master' + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Commit Not Found') + end + + it 'returns 404 if branch is not found' do + post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'foo' + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Branch Not Found') + end + + it 'returns 400 for missing parameters' do + post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user) + + expect(response).to have_http_status(400) + expect(json_response['error']).to eq('branch is missing') + end + end + + context 'unauthorized user' do + it 'does not cherry pick the commit' do + post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick"), branch: 'master' + + expect(response).to have_http_status(401) + end + end + end + + describe 'Post comment to commit' do + context 'authorized user' do + it 'returns comment' do + post v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment' + expect(response).to have_http_status(201) + expect(json_response['note']).to eq('My comment') + expect(json_response['path']).to be_nil + expect(json_response['line']).to be_nil + expect(json_response['line_type']).to be_nil + end + + it 'returns the inline comment' do + post v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment', path: project.repository.commit.raw_diffs.first.new_path, line: 1, line_type: 'new' + + expect(response).to have_http_status(201) + expect(json_response['note']).to eq('My comment') + expect(json_response['path']).to eq(project.repository.commit.raw_diffs.first.new_path) + expect(json_response['line']).to eq(1) + expect(json_response['line_type']).to eq('new') + end + + it 'returns 400 if note is missing' do + post v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user) + expect(response).to have_http_status(400) + end + + it 'returns 404 if note is attached to non existent commit' do + post v3_api("/projects/#{project.id}/repository/commits/1234ab/comments", user), note: 'My comment' + expect(response).to have_http_status(404) + end + end + + context 'unauthorized user' do + it 'does not return the diff of the selected commit' do + post v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments") + expect(response).to have_http_status(401) + end + end + end +end diff --git a/spec/requests/api/v3/environments_spec.rb b/spec/requests/api/v3/environments_spec.rb new file mode 100644 index 00000000000..216192c9d34 --- /dev/null +++ b/spec/requests/api/v3/environments_spec.rb @@ -0,0 +1,165 @@ +require 'spec_helper' + +describe API::V3::Environments, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:non_member) { create(:user) } + let(:project) { create(:empty_project, :private, namespace: user.namespace) } + let!(:environment) { create(:environment, project: project) } + + before do + project.team << [user, :master] + end + + shared_examples 'a paginated resources' do + before do + # Fires the request + request + end + + it 'has pagination headers' do + expect(response.headers).to include('X-Total') + expect(response.headers).to include('X-Total-Pages') + expect(response.headers).to include('X-Per-Page') + expect(response.headers).to include('X-Page') + expect(response.headers).to include('X-Next-Page') + expect(response.headers).to include('X-Prev-Page') + expect(response.headers).to include('Link') + end + end + + describe 'GET /projects/:id/environments' do + context 'as member of the project' do + it_behaves_like 'a paginated resources' do + let(:request) { get v3_api("/projects/#{project.id}/environments", user) } + end + + it 'returns project environments' do + get v3_api("/projects/#{project.id}/environments", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + expect(json_response.first['name']).to eq(environment.name) + expect(json_response.first['external_url']).to eq(environment.external_url) + expect(json_response.first['project']['id']).to eq(project.id) + expect(json_response.first['project']['visibility_level']).to be_present + end + end + + context 'as non member' do + it 'returns a 404 status code' do + get v3_api("/projects/#{project.id}/environments", non_member) + + expect(response).to have_http_status(404) + end + end + end + + describe 'POST /projects/:id/environments' do + context 'as a member' do + it 'creates a environment with valid params' do + post v3_api("/projects/#{project.id}/environments", user), name: "mepmep" + + expect(response).to have_http_status(201) + expect(json_response['name']).to eq('mepmep') + expect(json_response['slug']).to eq('mepmep') + expect(json_response['external']).to be nil + end + + it 'requires name to be passed' do + post v3_api("/projects/#{project.id}/environments", user), external_url: 'test.gitlab.com' + + expect(response).to have_http_status(400) + end + + it 'returns a 400 if environment already exists' do + post v3_api("/projects/#{project.id}/environments", user), name: environment.name + + expect(response).to have_http_status(400) + end + + it 'returns a 400 if slug is specified' do + post v3_api("/projects/#{project.id}/environments", user), name: "foo", slug: "foo" + + expect(response).to have_http_status(400) + expect(json_response["error"]).to eq("slug is automatically generated and cannot be changed") + end + end + + context 'a non member' do + it 'rejects the request' do + post v3_api("/projects/#{project.id}/environments", non_member), name: 'gitlab.com' + + expect(response).to have_http_status(404) + end + + it 'returns a 400 when the required params are missing' do + post v3_api("/projects/12345/environments", non_member), external_url: 'http://env.git.com' + end + end + end + + describe 'PUT /projects/:id/environments/:environment_id' do + it 'returns a 200 if name and external_url are changed' do + url = 'https://mepmep.whatever.ninja' + put v3_api("/projects/#{project.id}/environments/#{environment.id}", user), + name: 'Mepmep', external_url: url + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq('Mepmep') + expect(json_response['external_url']).to eq(url) + end + + it "won't allow slug to be changed" do + slug = environment.slug + api_url = v3_api("/projects/#{project.id}/environments/#{environment.id}", user) + put api_url, slug: slug + "-foo" + + expect(response).to have_http_status(400) + expect(json_response["error"]).to eq("slug is automatically generated and cannot be changed") + end + + it "won't update the external_url if only the name is passed" do + url = environment.external_url + put v3_api("/projects/#{project.id}/environments/#{environment.id}", user), + name: 'Mepmep' + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq('Mepmep') + expect(json_response['external_url']).to eq(url) + end + + it 'returns a 404 if the environment does not exist' do + put v3_api("/projects/#{project.id}/environments/12345", user) + + expect(response).to have_http_status(404) + end + end + + describe 'DELETE /projects/:id/environments/:environment_id' do + context 'as a master' do + it 'returns a 200 for an existing environment' do + delete v3_api("/projects/#{project.id}/environments/#{environment.id}", user) + + expect(response).to have_http_status(200) + end + + it 'returns a 404 for non existing id' do + delete v3_api("/projects/#{project.id}/environments/12345", user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Not found') + end + end + + context 'a non member' do + it 'rejects the request' do + delete v3_api("/projects/#{project.id}/environments/#{environment.id}", non_member) + + expect(response).to have_http_status(404) + end + end + end +end diff --git a/spec/requests/api/v3/files_spec.rb b/spec/requests/api/v3/files_spec.rb new file mode 100644 index 00000000000..3b61139a2cd --- /dev/null +++ b/spec/requests/api/v3/files_spec.rb @@ -0,0 +1,285 @@ +require 'spec_helper' + +describe API::V3::Files, api: true do + include ApiHelpers + + # I have to remove periods from the end of the name + # This happened when the user's name had a suffix (i.e. "Sr.") + # This seems to be what git does under the hood. For example, this commit: + # + # $ git commit --author='Foo Sr. <foo@example.com>' -m 'Where's my trailing period?' + # + # results in this: + # + # $ git show --pretty + # ... + # Author: Foo Sr <foo@example.com> + # ... + + let(:user) { create(:user) } + let!(:project) { create(:project, :repository, namespace: user.namespace ) } + let(:guest) { create(:user) { |u| project.add_guest(u) } } + let(:file_path) { 'files/ruby/popen.rb' } + let(:params) do + { + file_path: file_path, + ref: 'master' + } + end + let(:author_email) { FFaker::Internet.email } + let(:author_name) { FFaker::Name.name.chomp("\.") } + + before { project.team << [user, :developer] } + + describe "GET /projects/:id/repository/files" do + let(:route) { "/projects/#{project.id}/repository/files" } + + shared_examples_for 'repository files' do + it "returns file info" do + get v3_api(route, current_user), params + + expect(response).to have_http_status(200) + expect(json_response['file_path']).to eq(file_path) + expect(json_response['file_name']).to eq('popen.rb') + expect(json_response['last_commit_id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') + expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n") + end + + context 'when no params are given' do + it_behaves_like '400 response' do + let(:request) { get v3_api(route, current_user) } + end + end + + context 'when file_path does not exist' do + let(:params) do + { + file_path: 'app/models/application.rb', + ref: 'master', + } + end + + it_behaves_like '404 response' do + let(:request) { get v3_api(route, current_user), params } + let(:message) { '404 File Not Found' } + end + end + + context 'when repository is disabled' do + include_context 'disabled repository' + + it_behaves_like '403 response' do + let(:request) { get v3_api(route, current_user), params } + end + end + end + + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository files' do + let(:project) { create(:project, :public) } + let(:current_user) { nil } + end + end + + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get v3_api(route), params } + let(:message) { '404 Project Not Found' } + end + end + + context 'when authenticated', 'as a developer' do + it_behaves_like 'repository files' do + let(:current_user) { user } + end + end + + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get v3_api(route, guest), params } + end + end + end + + describe "POST /projects/:id/repository/files" do + let(:valid_params) do + { + file_path: 'newfile.rb', + branch_name: 'master', + content: 'puts 8', + commit_message: 'Added newfile' + } + end + + it "creates a new file in project repo" do + post v3_api("/projects/#{project.id}/repository/files", user), valid_params + + expect(response).to have_http_status(201) + expect(json_response['file_path']).to eq('newfile.rb') + last_commit = project.repository.commit.raw + expect(last_commit.author_email).to eq(user.email) + expect(last_commit.author_name).to eq(user.name) + end + + it "returns a 400 bad request if no params given" do + post v3_api("/projects/#{project.id}/repository/files", user) + + expect(response).to have_http_status(400) + end + + it "returns a 400 if editor fails to create file" do + allow_any_instance_of(Repository).to receive(:create_file). + and_return(false) + + post v3_api("/projects/#{project.id}/repository/files", user), valid_params + + expect(response).to have_http_status(400) + end + + context "when specifying an author" do + it "creates a new file with the specified author" do + valid_params.merge!(author_email: author_email, author_name: author_name) + + post v3_api("/projects/#{project.id}/repository/files", user), valid_params + + expect(response).to have_http_status(201) + last_commit = project.repository.commit.raw + expect(last_commit.author_email).to eq(author_email) + expect(last_commit.author_name).to eq(author_name) + end + end + + context 'when the repo is empty' do + let!(:project) { create(:project_empty_repo, namespace: user.namespace ) } + + it "creates a new file in project repo" do + post v3_api("/projects/#{project.id}/repository/files", user), valid_params + + expect(response).to have_http_status(201) + expect(json_response['file_path']).to eq('newfile.rb') + last_commit = project.repository.commit.raw + expect(last_commit.author_email).to eq(user.email) + expect(last_commit.author_name).to eq(user.name) + end + end + end + + describe "PUT /projects/:id/repository/files" do + let(:valid_params) do + { + file_path: file_path, + branch_name: 'master', + content: 'puts 8', + commit_message: 'Changed file' + } + end + + it "updates existing file in project repo" do + put v3_api("/projects/#{project.id}/repository/files", user), valid_params + + expect(response).to have_http_status(200) + expect(json_response['file_path']).to eq(file_path) + last_commit = project.repository.commit.raw + expect(last_commit.author_email).to eq(user.email) + expect(last_commit.author_name).to eq(user.name) + end + + it "returns a 400 bad request if no params given" do + put v3_api("/projects/#{project.id}/repository/files", user) + + expect(response).to have_http_status(400) + end + + context "when specifying an author" do + it "updates a file with the specified author" do + valid_params.merge!(author_email: author_email, author_name: author_name, content: "New content") + + put v3_api("/projects/#{project.id}/repository/files", user), valid_params + + expect(response).to have_http_status(200) + last_commit = project.repository.commit.raw + expect(last_commit.author_email).to eq(author_email) + expect(last_commit.author_name).to eq(author_name) + end + end + end + + describe "DELETE /projects/:id/repository/files" do + let(:valid_params) do + { + file_path: file_path, + branch_name: 'master', + commit_message: 'Changed file' + } + end + + it "deletes existing file in project repo" do + delete v3_api("/projects/#{project.id}/repository/files", user), valid_params + + expect(response).to have_http_status(200) + expect(json_response['file_path']).to eq(file_path) + last_commit = project.repository.commit.raw + expect(last_commit.author_email).to eq(user.email) + expect(last_commit.author_name).to eq(user.name) + end + + it "returns a 400 bad request if no params given" do + delete v3_api("/projects/#{project.id}/repository/files", user) + + expect(response).to have_http_status(400) + end + + it "returns a 400 if fails to create file" do + allow_any_instance_of(Repository).to receive(:delete_file).and_return(false) + + delete v3_api("/projects/#{project.id}/repository/files", user), valid_params + + expect(response).to have_http_status(400) + end + + context "when specifying an author" do + it "removes a file with the specified author" do + valid_params.merge!(author_email: author_email, author_name: author_name) + + delete v3_api("/projects/#{project.id}/repository/files", user), valid_params + + expect(response).to have_http_status(200) + last_commit = project.repository.commit.raw + expect(last_commit.author_email).to eq(author_email) + expect(last_commit.author_name).to eq(author_name) + end + end + end + + describe "POST /projects/:id/repository/files with binary file" do + let(:file_path) { 'test.bin' } + let(:put_params) do + { + file_path: file_path, + branch_name: 'master', + content: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=', + commit_message: 'Binary file with a \n should not be touched', + encoding: 'base64' + } + end + let(:get_params) do + { + file_path: file_path, + ref: 'master', + } + end + + before do + post v3_api("/projects/#{project.id}/repository/files", user), put_params + end + + it "remains unchanged" do + get v3_api("/projects/#{project.id}/repository/files", user), get_params + + expect(response).to have_http_status(200) + expect(json_response['file_path']).to eq(file_path) + expect(json_response['file_name']).to eq(file_path) + expect(json_response['content']).to eq(put_params[:content]) + end + end +end diff --git a/spec/requests/api/v3/groups_spec.rb b/spec/requests/api/v3/groups_spec.rb new file mode 100644 index 00000000000..a71b7d4b008 --- /dev/null +++ b/spec/requests/api/v3/groups_spec.rb @@ -0,0 +1,565 @@ +require 'spec_helper' + +describe API::V3::Groups, api: true do + include ApiHelpers + include UploadHelpers + + let(:user1) { create(:user, can_create_group: false) } + let(:user2) { create(:user) } + let(:user3) { create(:user) } + let(:admin) { create(:admin) } + let!(:group1) { create(:group, avatar: File.open(uploaded_image_temp_path)) } + let!(:group2) { create(:group, :private) } + let!(:project1) { create(:empty_project, namespace: group1) } + let!(:project2) { create(:empty_project, namespace: group2) } + let!(:project3) { create(:empty_project, namespace: group1, path: 'test', visibility_level: Gitlab::VisibilityLevel::PRIVATE) } + + before do + group1.add_owner(user1) + group2.add_owner(user2) + end + + describe "GET /groups" do + context "when unauthenticated" do + it "returns authentication error" do + get v3_api("/groups") + + expect(response).to have_http_status(401) + end + end + + context "when authenticated as user" do + it "normal user: returns an array of groups of user1" do + get v3_api("/groups", user1) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response) + .to satisfy_one { |group| group['name'] == group1.name } + end + + it "does not include statistics" do + get v3_api("/groups", user1), statistics: true + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first).not_to include 'statistics' + end + end + + context "when authenticated as admin" do + it "admin: returns an array of all groups" do + get v3_api("/groups", admin) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + end + + it "does not include statistics by default" do + get v3_api("/groups", admin) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first).not_to include('statistics') + end + + it "includes statistics if requested" do + attributes = { + storage_size: 702, + repository_size: 123, + lfs_objects_size: 234, + build_artifacts_size: 345, + }.stringify_keys + + project1.statistics.update!(attributes) + + get v3_api("/groups", admin), statistics: true + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response) + .to satisfy_one { |group| group['statistics'] == attributes } + end + end + + context "when using skip_groups in request" do + it "returns all groups excluding skipped groups" do + get v3_api("/groups", admin), skip_groups: [group2.id] + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + end + end + + context "when using all_available in request" do + let(:response_groups) { json_response.map { |group| group['name'] } } + + it "returns all groups you have access to" do + public_group = create :group, :public + + get v3_api("/groups", user1), all_available: true + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_groups).to contain_exactly(public_group.name, group1.name) + end + end + + context "when using sorting" do + let(:group3) { create(:group, name: "a#{group1.name}", path: "z#{group1.path}") } + let(:response_groups) { json_response.map { |group| group['name'] } } + + before do + group3.add_owner(user1) + end + + it "sorts by name ascending by default" do + get v3_api("/groups", user1) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_groups).to eq([group3.name, group1.name]) + end + + it "sorts in descending order when passed" do + get v3_api("/groups", user1), sort: "desc" + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_groups).to eq([group1.name, group3.name]) + end + + it "sorts by the order_by param" do + get v3_api("/groups", user1), order_by: "path" + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_groups).to eq([group1.name, group3.name]) + end + end + end + + describe 'GET /groups/owned' do + context 'when unauthenticated' do + it 'returns authentication error' do + get v3_api('/groups/owned') + + expect(response).to have_http_status(401) + end + end + + context 'when authenticated as group owner' do + it 'returns an array of groups the user owns' do + get v3_api('/groups/owned', user2) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(group2.name) + end + end + end + + describe "GET /groups/:id" do + context "when authenticated as user" do + it "returns one of user1's groups" do + project = create(:empty_project, namespace: group2, path: 'Foo') + create(:project_group_link, project: project, group: group1) + + get v3_api("/groups/#{group1.id}", user1) + + expect(response).to have_http_status(200) + expect(json_response['id']).to eq(group1.id) + expect(json_response['name']).to eq(group1.name) + expect(json_response['path']).to eq(group1.path) + expect(json_response['description']).to eq(group1.description) + expect(json_response['visibility_level']).to eq(group1.visibility_level) + expect(json_response['avatar_url']).to eq(group1.avatar_url) + expect(json_response['web_url']).to eq(group1.web_url) + expect(json_response['request_access_enabled']).to eq(group1.request_access_enabled) + expect(json_response['full_name']).to eq(group1.full_name) + expect(json_response['full_path']).to eq(group1.full_path) + expect(json_response['parent_id']).to eq(group1.parent_id) + expect(json_response['projects']).to be_an Array + expect(json_response['projects'].length).to eq(2) + expect(json_response['shared_projects']).to be_an Array + expect(json_response['shared_projects'].length).to eq(1) + expect(json_response['shared_projects'][0]['id']).to eq(project.id) + end + + it "does not return a non existing group" do + get v3_api("/groups/1328", user1) + + expect(response).to have_http_status(404) + end + + it "does not return a group not attached to user1" do + get v3_api("/groups/#{group2.id}", user1) + + expect(response).to have_http_status(404) + end + end + + context "when authenticated as admin" do + it "returns any existing group" do + get v3_api("/groups/#{group2.id}", admin) + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(group2.name) + end + + it "does not return a non existing group" do + get v3_api("/groups/1328", admin) + + expect(response).to have_http_status(404) + end + end + + context 'when using group path in URL' do + it 'returns any existing group' do + get v3_api("/groups/#{group1.path}", admin) + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(group1.name) + end + + it 'does not return a non existing group' do + get v3_api('/groups/unknown', admin) + + expect(response).to have_http_status(404) + end + + it 'does not return a group not attached to user1' do + get v3_api("/groups/#{group2.path}", user1) + + expect(response).to have_http_status(404) + end + end + end + + describe 'PUT /groups/:id' do + let(:new_group_name) { 'New Group'} + + context 'when authenticated as the group owner' do + it 'updates the group' do + put v3_api("/groups/#{group1.id}", user1), name: new_group_name, request_access_enabled: true + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(new_group_name) + expect(json_response['request_access_enabled']).to eq(true) + end + + it 'returns 404 for a non existing group' do + put v3_api('/groups/1328', user1), name: new_group_name + + expect(response).to have_http_status(404) + end + end + + context 'when authenticated as the admin' do + it 'updates the group' do + put v3_api("/groups/#{group1.id}", admin), name: new_group_name + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(new_group_name) + end + end + + context 'when authenticated as an user that can see the group' do + it 'does not updates the group' do + put v3_api("/groups/#{group1.id}", user2), name: new_group_name + + expect(response).to have_http_status(403) + end + end + + context 'when authenticated as an user that cannot see the group' do + it 'returns 404 when trying to update the group' do + put v3_api("/groups/#{group2.id}", user1), name: new_group_name + + expect(response).to have_http_status(404) + end + end + end + + describe "GET /groups/:id/projects" do + context "when authenticated as user" do + it "returns the group's projects" do + get v3_api("/groups/#{group1.id}/projects", user1) + + expect(response).to have_http_status(200) + expect(json_response.length).to eq(2) + project_names = json_response.map { |proj| proj['name'] } + expect(project_names).to match_array([project1.name, project3.name]) + expect(json_response.first['visibility_level']).to be_present + end + + it "returns the group's projects with simple representation" do + get v3_api("/groups/#{group1.id}/projects", user1), simple: true + + expect(response).to have_http_status(200) + expect(json_response.length).to eq(2) + project_names = json_response.map { |proj| proj['name'] } + expect(project_names).to match_array([project1.name, project3.name]) + expect(json_response.first['visibility_level']).not_to be_present + end + + it 'filters the groups projects' do + public_project = create(:empty_project, :public, path: 'test1', group: group1) + + get v3_api("/groups/#{group1.id}/projects", user1), visibility: 'public' + + expect(response).to have_http_status(200) + expect(json_response).to be_an(Array) + expect(json_response.length).to eq(1) + expect(json_response.first['name']).to eq(public_project.name) + end + + it "does not return a non existing group" do + get v3_api("/groups/1328/projects", user1) + + expect(response).to have_http_status(404) + end + + it "does not return a group not attached to user1" do + get v3_api("/groups/#{group2.id}/projects", user1) + + expect(response).to have_http_status(404) + end + + it "only returns projects to which user has access" do + project3.team << [user3, :developer] + + get v3_api("/groups/#{group1.id}/projects", user3) + + expect(response).to have_http_status(200) + expect(json_response.length).to eq(1) + expect(json_response.first['name']).to eq(project3.name) + end + + it 'only returns the projects owned by user' do + project2.group.add_owner(user3) + + get v3_api("/groups/#{project2.group.id}/projects", user3), owned: true + + expect(response).to have_http_status(200) + expect(json_response.length).to eq(1) + expect(json_response.first['name']).to eq(project2.name) + end + + it 'only returns the projects starred by user' do + user1.starred_projects = [project1] + + get v3_api("/groups/#{group1.id}/projects", user1), starred: true + + expect(response).to have_http_status(200) + expect(json_response.length).to eq(1) + expect(json_response.first['name']).to eq(project1.name) + end + end + + context "when authenticated as admin" do + it "returns any existing group" do + get v3_api("/groups/#{group2.id}/projects", admin) + + expect(response).to have_http_status(200) + expect(json_response.length).to eq(1) + expect(json_response.first['name']).to eq(project2.name) + end + + it "does not return a non existing group" do + get v3_api("/groups/1328/projects", admin) + + expect(response).to have_http_status(404) + end + end + + context 'when using group path in URL' do + it 'returns any existing group' do + get v3_api("/groups/#{group1.path}/projects", admin) + + expect(response).to have_http_status(200) + project_names = json_response.map { |proj| proj['name'] } + expect(project_names).to match_array([project1.name, project3.name]) + end + + it 'does not return a non existing group' do + get v3_api('/groups/unknown/projects', admin) + + expect(response).to have_http_status(404) + end + + it 'does not return a group not attached to user1' do + get v3_api("/groups/#{group2.path}/projects", user1) + + expect(response).to have_http_status(404) + end + end + end + + describe "POST /groups" do + context "when authenticated as user without group permissions" do + it "does not create group" do + post v3_api("/groups", user1), attributes_for(:group) + + expect(response).to have_http_status(403) + end + end + + context "when authenticated as user with group permissions" do + it "creates group" do + group = attributes_for(:group, { request_access_enabled: false }) + + post v3_api("/groups", user3), group + + expect(response).to have_http_status(201) + + expect(json_response["name"]).to eq(group[:name]) + expect(json_response["path"]).to eq(group[:path]) + expect(json_response["request_access_enabled"]).to eq(group[:request_access_enabled]) + end + + it "creates a nested group" do + parent = create(:group) + parent.add_owner(user3) + group = attributes_for(:group, { parent_id: parent.id }) + + post v3_api("/groups", user3), group + + expect(response).to have_http_status(201) + + expect(json_response["full_path"]).to eq("#{parent.path}/#{group[:path]}") + expect(json_response["parent_id"]).to eq(parent.id) + end + + it "does not create group, duplicate" do + post v3_api("/groups", user3), { name: 'Duplicate Test', path: group2.path } + + expect(response).to have_http_status(400) + expect(response.message).to eq("Bad Request") + end + + it "returns 400 bad request error if name not given" do + post v3_api("/groups", user3), { path: group2.path } + + expect(response).to have_http_status(400) + end + + it "returns 400 bad request error if path not given" do + post v3_api("/groups", user3), { name: 'test' } + + expect(response).to have_http_status(400) + end + end + end + + describe "DELETE /groups/:id" do + context "when authenticated as user" do + it "removes group" do + delete v3_api("/groups/#{group1.id}", user1) + + expect(response).to have_http_status(200) + end + + it "does not remove a group if not an owner" do + user4 = create(:user) + group1.add_master(user4) + + delete v3_api("/groups/#{group1.id}", user3) + + expect(response).to have_http_status(403) + end + + it "does not remove a non existing group" do + delete v3_api("/groups/1328", user1) + + expect(response).to have_http_status(404) + end + + it "does not remove a group not attached to user1" do + delete v3_api("/groups/#{group2.id}", user1) + + expect(response).to have_http_status(404) + end + end + + context "when authenticated as admin" do + it "removes any existing group" do + delete v3_api("/groups/#{group2.id}", admin) + + expect(response).to have_http_status(200) + end + + it "does not remove a non existing group" do + delete v3_api("/groups/1328", admin) + + expect(response).to have_http_status(404) + end + end + end + + describe "POST /groups/:id/projects/:project_id" do + let(:project) { create(:empty_project) } + let(:project_path) { "#{project.namespace.path}%2F#{project.path}" } + + before(:each) do + allow_any_instance_of(Projects::TransferService). + to receive(:execute).and_return(true) + end + + context "when authenticated as user" do + it "does not transfer project to group" do + post v3_api("/groups/#{group1.id}/projects/#{project.id}", user2) + + expect(response).to have_http_status(403) + end + end + + context "when authenticated as admin" do + it "transfers project to group" do + post v3_api("/groups/#{group1.id}/projects/#{project.id}", admin) + + expect(response).to have_http_status(201) + end + + context 'when using project path in URL' do + context 'with a valid project path' do + it "transfers project to group" do + post v3_api("/groups/#{group1.id}/projects/#{project_path}", admin) + + expect(response).to have_http_status(201) + end + end + + context 'with a non-existent project path' do + it "does not transfer project to group" do + post v3_api("/groups/#{group1.id}/projects/nogroup%2Fnoproject", admin) + + expect(response).to have_http_status(404) + end + end + end + + context 'when using a group path in URL' do + context 'with a valid group path' do + it "transfers project to group" do + post v3_api("/groups/#{group1.path}/projects/#{project_path}", admin) + + expect(response).to have_http_status(201) + end + end + + context 'with a non-existent group path' do + it "does not transfer project to group" do + post v3_api("/groups/noexist/projects/#{project_path}", admin) + + expect(response).to have_http_status(404) + end + end + end + end + end +end diff --git a/spec/requests/api/v3/issues_spec.rb b/spec/requests/api/v3/issues_spec.rb index 33a127de98a..803acd55470 100644 --- a/spec/requests/api/v3/issues_spec.rb +++ b/spec/requests/api/v3/issues_spec.rb @@ -722,7 +722,7 @@ describe API::V3::Issues, api: true do 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['labels']).to eq(%w(label label2)) expect(json_response['confidential']).to be_falsy end @@ -986,6 +986,33 @@ describe API::V3::Issues, api: true do end end + describe 'PUT /projects/:id/issues/:issue_id with spam filtering' do + let(:params) do + { + title: 'updated title', + description: 'content here', + labels: 'label, label2' + } + end + + it "does not create a new project issue" do + allow_any_instance_of(SpamService).to receive_messages(check_for_spam?: true) + allow_any_instance_of(AkismetService).to receive_messages(is_spam?: true) + + put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), params + + expect(response).to have_http_status(400) + expect(json_response['message']).to eq({ "error" => "Spam detected" }) + + spam_logs = SpamLog.all + expect(spam_logs.count).to eq(1) + expect(spam_logs[0].title).to eq('updated title') + expect(spam_logs[0].description).to eq('content here') + expect(spam_logs[0].user).to eq(user) + expect(spam_logs[0].noteable_type).to eq('Issue') + end + end + describe 'PUT /projects/:id/issues/:issue_id to update labels' do let!(:label) { create(:label, title: 'dummy', project: project) } let!(:label_link) { create(:label_link, label: label, target: issue) } diff --git a/spec/requests/api/v3/labels_spec.rb b/spec/requests/api/v3/labels_spec.rb new file mode 100644 index 00000000000..dfac357d37c --- /dev/null +++ b/spec/requests/api/v3/labels_spec.rb @@ -0,0 +1,171 @@ +require 'spec_helper' + +describe API::V3::Labels, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) } + let!(:label1) { create(:label, title: 'label1', project: project) } + let!(:priority_label) { create(:label, title: 'bug', project: project, priority: 3) } + + before do + project.team << [user, :master] + end + + describe 'GET /projects/:id/labels' do + it 'returns all available labels to the project' do + group = create(:group) + group_label = create(:group_label, title: 'feature', group: group) + project.update(group: group) + create(:labeled_issue, project: project, labels: [group_label], author: user) + create(:labeled_issue, project: project, labels: [label1], author: user, state: :closed) + create(:labeled_merge_request, labels: [priority_label], author: user, source_project: project ) + + expected_keys = %w( + id name color description + open_issues_count closed_issues_count open_merge_requests_count + subscribed priority + ) + + get v3_api("/projects/#{project.id}/labels", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(3) + expect(json_response.first.keys).to match_array expected_keys + expect(json_response.map { |l| l['name'] }).to match_array([group_label.name, priority_label.name, label1.name]) + + label1_response = json_response.find { |l| l['name'] == label1.title } + group_label_response = json_response.find { |l| l['name'] == group_label.title } + priority_label_response = json_response.find { |l| l['name'] == priority_label.title } + + expect(label1_response['open_issues_count']).to eq(0) + expect(label1_response['closed_issues_count']).to eq(1) + expect(label1_response['open_merge_requests_count']).to eq(0) + expect(label1_response['name']).to eq(label1.name) + expect(label1_response['color']).to be_present + expect(label1_response['description']).to be_nil + expect(label1_response['priority']).to be_nil + expect(label1_response['subscribed']).to be_falsey + + expect(group_label_response['open_issues_count']).to eq(1) + expect(group_label_response['closed_issues_count']).to eq(0) + expect(group_label_response['open_merge_requests_count']).to eq(0) + expect(group_label_response['name']).to eq(group_label.name) + expect(group_label_response['color']).to be_present + expect(group_label_response['description']).to be_nil + expect(group_label_response['priority']).to be_nil + expect(group_label_response['subscribed']).to be_falsey + + expect(priority_label_response['open_issues_count']).to eq(0) + expect(priority_label_response['closed_issues_count']).to eq(0) + expect(priority_label_response['open_merge_requests_count']).to eq(1) + expect(priority_label_response['name']).to eq(priority_label.name) + expect(priority_label_response['color']).to be_present + expect(priority_label_response['description']).to be_nil + expect(priority_label_response['priority']).to eq(3) + expect(priority_label_response['subscribed']).to be_falsey + end + end + + describe "POST /projects/:id/labels/:label_id/subscription" do + context "when label_id is a label title" do + it "subscribes to the label" do + post v3_api("/projects/#{project.id}/labels/#{label1.title}/subscription", user) + + expect(response).to have_http_status(201) + expect(json_response["name"]).to eq(label1.title) + expect(json_response["subscribed"]).to be_truthy + end + end + + context "when label_id is a label ID" do + it "subscribes to the label" do + post v3_api("/projects/#{project.id}/labels/#{label1.id}/subscription", user) + + expect(response).to have_http_status(201) + expect(json_response["name"]).to eq(label1.title) + expect(json_response["subscribed"]).to be_truthy + end + end + + context "when user is already subscribed to label" do + before { label1.subscribe(user, project) } + + it "returns 304" do + post v3_api("/projects/#{project.id}/labels/#{label1.id}/subscription", user) + + expect(response).to have_http_status(304) + end + end + + context "when label ID is not found" do + it "returns 404 error" do + post v3_api("/projects/#{project.id}/labels/1234/subscription", user) + + expect(response).to have_http_status(404) + end + end + end + + describe "DELETE /projects/:id/labels/:label_id/subscription" do + before { label1.subscribe(user, project) } + + context "when label_id is a label title" do + it "unsubscribes from the label" do + delete v3_api("/projects/#{project.id}/labels/#{label1.title}/subscription", user) + + expect(response).to have_http_status(200) + expect(json_response["name"]).to eq(label1.title) + expect(json_response["subscribed"]).to be_falsey + end + end + + context "when label_id is a label ID" do + it "unsubscribes from the label" do + delete v3_api("/projects/#{project.id}/labels/#{label1.id}/subscription", user) + + expect(response).to have_http_status(200) + expect(json_response["name"]).to eq(label1.title) + expect(json_response["subscribed"]).to be_falsey + end + end + + context "when user is already unsubscribed from label" do + before { label1.unsubscribe(user, project) } + + it "returns 304" do + delete v3_api("/projects/#{project.id}/labels/#{label1.id}/subscription", user) + + expect(response).to have_http_status(304) + end + end + + context "when label ID is not found" do + it "returns 404 error" do + delete v3_api("/projects/#{project.id}/labels/1234/subscription", user) + + expect(response).to have_http_status(404) + end + end + end + + describe 'DELETE /projects/:id/labels' do + it 'returns 200 for existing label' do + delete v3_api("/projects/#{project.id}/labels", user), name: 'label1' + + expect(response).to have_http_status(200) + end + + it 'returns 404 for non existing label' do + delete v3_api("/projects/#{project.id}/labels", user), name: 'label2' + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Label Not Found') + end + + it 'returns 400 for wrong parameters' do + delete v3_api("/projects/#{project.id}/labels", user) + expect(response).to have_http_status(400) + end + end +end diff --git a/spec/requests/api/v3/members_spec.rb b/spec/requests/api/v3/members_spec.rb new file mode 100644 index 00000000000..13814ed10c3 --- /dev/null +++ b/spec/requests/api/v3/members_spec.rb @@ -0,0 +1,342 @@ +require 'spec_helper' + +describe API::V3::Members, api: true do + include ApiHelpers + + let(:master) { create(:user) } + let(:developer) { create(:user) } + let(:access_requester) { create(:user) } + let(:stranger) { create(:user) } + + let(:project) do + create(:empty_project, :public, :access_requestable, creator_id: master.id, namespace: master.namespace) do |project| + project.team << [developer, :developer] + project.team << [master, :master] + project.request_access(access_requester) + end + end + + let!(:group) do + create(:group, :public, :access_requestable) do |group| + group.add_developer(developer) + group.add_owner(master) + group.request_access(access_requester) + end + end + + shared_examples 'GET /:sources/:id/members' do |source_type| + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) { get v3_api("/#{source_type.pluralize}/#{source.id}/members", stranger) } + end + + %i[master developer access_requester stranger].each do |type| + context "when authenticated as a #{type}" do + it 'returns 200' do + user = public_send(type) + get v3_api("/#{source_type.pluralize}/#{source.id}/members", user) + + expect(response).to have_http_status(200) + expect(json_response.size).to eq(2) + expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id] + end + end + end + + it 'does not return invitees' do + create(:"#{source_type}_member", invite_token: '123', invite_email: 'test@abc.com', source: source, user: nil) + + get v3_api("/#{source_type.pluralize}/#{source.id}/members", developer) + + expect(response).to have_http_status(200) + expect(json_response.size).to eq(2) + expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id] + end + + it 'finds members with query string' do + get v3_api("/#{source_type.pluralize}/#{source.id}/members", developer), query: master.username + + expect(response).to have_http_status(200) + expect(json_response.count).to eq(1) + expect(json_response.first['username']).to eq(master.username) + end + end + end + + shared_examples 'GET /:sources/:id/members/:user_id' do |source_type| + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) { get v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) } + end + + context 'when authenticated as a non-member' do + %i[access_requester stranger].each do |type| + context "as a #{type}" do + it 'returns 200' do + user = public_send(type) + get v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user) + + expect(response).to have_http_status(200) + # User attributes + expect(json_response['id']).to eq(developer.id) + expect(json_response['name']).to eq(developer.name) + expect(json_response['username']).to eq(developer.username) + expect(json_response['state']).to eq(developer.state) + expect(json_response['avatar_url']).to eq(developer.avatar_url) + expect(json_response['web_url']).to eq(Gitlab::Routing.url_helpers.user_url(developer)) + + # Member attributes + expect(json_response['access_level']).to eq(Member::DEVELOPER) + end + end + end + end + end + end + + shared_examples 'POST /:sources/:id/members' do |source_type| + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) do + post v3_api("/#{source_type.pluralize}/#{source.id}/members", stranger), + user_id: access_requester.id, access_level: Member::MASTER + end + end + + context 'when authenticated as a non-member or member with insufficient rights' do + %i[access_requester stranger developer].each do |type| + context "as a #{type}" do + it 'returns 403' do + user = public_send(type) + post v3_api("/#{source_type.pluralize}/#{source.id}/members", user), + user_id: access_requester.id, access_level: Member::MASTER + + expect(response).to have_http_status(403) + end + end + end + end + + context 'when authenticated as a master/owner' do + context 'and new member is already a requester' do + it 'transforms the requester into a proper member' do + expect do + post v3_api("/#{source_type.pluralize}/#{source.id}/members", master), + user_id: access_requester.id, access_level: Member::MASTER + + expect(response).to have_http_status(201) + end.to change { source.members.count }.by(1) + expect(source.requesters.count).to eq(0) + expect(json_response['id']).to eq(access_requester.id) + expect(json_response['access_level']).to eq(Member::MASTER) + end + end + + it 'creates a new member' do + expect do + post v3_api("/#{source_type.pluralize}/#{source.id}/members", master), + user_id: stranger.id, access_level: Member::DEVELOPER, expires_at: '2016-08-05' + + expect(response).to have_http_status(201) + end.to change { source.members.count }.by(1) + expect(json_response['id']).to eq(stranger.id) + expect(json_response['access_level']).to eq(Member::DEVELOPER) + expect(json_response['expires_at']).to eq('2016-08-05') + end + end + + it "returns #{source_type == 'project' ? 201 : 409} if member already exists" do + post v3_api("/#{source_type.pluralize}/#{source.id}/members", master), + user_id: master.id, access_level: Member::MASTER + + expect(response).to have_http_status(source_type == 'project' ? 201 : 409) + end + + it 'returns 400 when user_id is not given' do + post v3_api("/#{source_type.pluralize}/#{source.id}/members", master), + access_level: Member::MASTER + + expect(response).to have_http_status(400) + end + + it 'returns 400 when access_level is not given' do + post v3_api("/#{source_type.pluralize}/#{source.id}/members", master), + user_id: stranger.id + + expect(response).to have_http_status(400) + end + + it 'returns 422 when access_level is not valid' do + post v3_api("/#{source_type.pluralize}/#{source.id}/members", master), + user_id: stranger.id, access_level: 1234 + + expect(response).to have_http_status(422) + end + end + end + + shared_examples 'PUT /:sources/:id/members/:user_id' do |source_type| + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) do + put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger), + access_level: Member::MASTER + end + end + + context 'when authenticated as a non-member or member with insufficient rights' do + %i[access_requester stranger developer].each do |type| + context "as a #{type}" do + it 'returns 403' do + user = public_send(type) + put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user), + access_level: Member::MASTER + + expect(response).to have_http_status(403) + end + end + end + end + + context 'when authenticated as a master/owner' do + it 'updates the member' do + put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master), + access_level: Member::MASTER, expires_at: '2016-08-05' + + expect(response).to have_http_status(200) + expect(json_response['id']).to eq(developer.id) + expect(json_response['access_level']).to eq(Member::MASTER) + expect(json_response['expires_at']).to eq('2016-08-05') + end + end + + it 'returns 409 if member does not exist' do + put v3_api("/#{source_type.pluralize}/#{source.id}/members/123", master), + access_level: Member::MASTER + + expect(response).to have_http_status(404) + end + + it 'returns 400 when access_level is not given' do + put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master) + + expect(response).to have_http_status(400) + end + + it 'returns 422 when access level is not valid' do + put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master), + access_level: 1234 + + expect(response).to have_http_status(422) + end + end + end + + shared_examples 'DELETE /:sources/:id/members/:user_id' do |source_type| + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) { delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) } + end + + context 'when authenticated as a non-member or member with insufficient rights' do + %i[access_requester stranger].each do |type| + context "as a #{type}" do + it 'returns 403' do + user = public_send(type) + delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user) + + expect(response).to have_http_status(403) + end + end + end + end + + context 'when authenticated as a member and deleting themself' do + it 'deletes the member' do + expect do + delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", developer) + + expect(response).to have_http_status(200) + end.to change { source.members.count }.by(-1) + end + end + + context 'when authenticated as a master/owner' do + context 'and member is a requester' do + it "returns #{source_type == 'project' ? 200 : 404}" do + expect do + delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{access_requester.id}", master) + + expect(response).to have_http_status(source_type == 'project' ? 200 : 404) + end.not_to change { source.requesters.count } + end + end + + it 'deletes the member' do + expect do + delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master) + + expect(response).to have_http_status(200) + end.to change { source.members.count }.by(-1) + end + end + + it "returns #{source_type == 'project' ? 200 : 404} if member does not exist" do + delete v3_api("/#{source_type.pluralize}/#{source.id}/members/123", master) + + expect(response).to have_http_status(source_type == 'project' ? 200 : 404) + end + end + end + + it_behaves_like 'GET /:sources/:id/members', 'project' do + let(:source) { project } + end + + it_behaves_like 'GET /:sources/:id/members', 'group' do + let(:source) { group } + end + + it_behaves_like 'GET /:sources/:id/members/:user_id', 'project' do + let(:source) { project } + end + + it_behaves_like 'GET /:sources/:id/members/:user_id', 'group' do + let(:source) { group } + end + + it_behaves_like 'POST /:sources/:id/members', 'project' do + let(:source) { project } + end + + it_behaves_like 'POST /:sources/:id/members', 'group' do + let(:source) { group } + end + + it_behaves_like 'PUT /:sources/:id/members/:user_id', 'project' do + let(:source) { project } + end + + it_behaves_like 'PUT /:sources/:id/members/:user_id', 'group' do + let(:source) { group } + end + + it_behaves_like 'DELETE /:sources/:id/members/:user_id', 'project' do + let(:source) { project } + end + + it_behaves_like 'DELETE /:sources/:id/members/:user_id', 'group' do + let(:source) { group } + end + + context 'Adding owner to project' do + it 'returns 403' do + expect do + post v3_api("/projects/#{project.id}/members", master), + user_id: stranger.id, access_level: Member::OWNER + + expect(response).to have_http_status(422) + end.to change { project.members.count }.by(0) + end + end +end diff --git a/spec/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb index b94e1ef4ced..51764d1000e 100644 --- a/spec/requests/api/v3/merge_requests_spec.rb +++ b/spec/requests/api/v3/merge_requests_spec.rb @@ -237,7 +237,7 @@ describe API::MergeRequests, api: true do expect(response).to have_http_status(201) expect(json_response['title']).to eq('Test merge_request') - expect(json_response['labels']).to eq(['label', 'label2']) + expect(json_response['labels']).to eq(%w(label label2)) expect(json_response['milestone']['id']).to eq(milestone.id) expect(json_response['force_remove_source_branch']).to be_truthy end diff --git a/spec/requests/api/v3/milestones_spec.rb b/spec/requests/api/v3/milestones_spec.rb new file mode 100644 index 00000000000..77705d8c839 --- /dev/null +++ b/spec/requests/api/v3/milestones_spec.rb @@ -0,0 +1,232 @@ +require 'spec_helper' + +describe API::V3::Milestones, api: true do + include ApiHelpers + let(:user) { create(:user) } + let!(:project) { create(:empty_project, namespace: user.namespace ) } + let!(:closed_milestone) { create(:closed_milestone, project: project) } + let!(:milestone) { create(:milestone, project: project) } + + before { project.team << [user, :developer] } + + describe 'GET /projects/:id/milestones' do + it 'returns project milestones' do + get v3_api("/projects/#{project.id}/milestones", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['title']).to eq(milestone.title) + end + + it 'returns a 401 error if user not authenticated' do + get v3_api("/projects/#{project.id}/milestones") + + expect(response).to have_http_status(401) + end + + it 'returns an array of active milestones' do + get v3_api("/projects/#{project.id}/milestones?state=active", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(milestone.id) + end + + it 'returns an array of closed milestones' do + get v3_api("/projects/#{project.id}/milestones?state=closed", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(closed_milestone.id) + end + end + + describe 'GET /projects/:id/milestones/:milestone_id' do + it 'returns a project milestone by id' do + get v3_api("/projects/#{project.id}/milestones/#{milestone.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['title']).to eq(milestone.title) + expect(json_response['iid']).to eq(milestone.iid) + end + + it 'returns a project milestone by iid' do + get v3_api("/projects/#{project.id}/milestones?iid=#{closed_milestone.iid}", user) + + expect(response.status).to eq 200 + expect(json_response.size).to eq(1) + expect(json_response.first['title']).to eq closed_milestone.title + expect(json_response.first['id']).to eq closed_milestone.id + end + + it 'returns a project milestone by iid array' do + get v3_api("/projects/#{project.id}/milestones", user), iid: [milestone.iid, closed_milestone.iid] + + expect(response).to have_http_status(200) + expect(json_response.size).to eq(2) + expect(json_response.first['title']).to eq milestone.title + expect(json_response.first['id']).to eq milestone.id + end + + it 'returns 401 error if user not authenticated' do + get v3_api("/projects/#{project.id}/milestones/#{milestone.id}") + + expect(response).to have_http_status(401) + end + + it 'returns a 404 error if milestone id not found' do + get v3_api("/projects/#{project.id}/milestones/1234", user) + + expect(response).to have_http_status(404) + end + end + + describe 'POST /projects/:id/milestones' do + it 'creates a new project milestone' do + post v3_api("/projects/#{project.id}/milestones", user), title: 'new milestone' + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq('new milestone') + expect(json_response['description']).to be_nil + end + + it 'creates a new project milestone with description and dates' do + post v3_api("/projects/#{project.id}/milestones", user), + title: 'new milestone', description: 'release', due_date: '2013-03-02', start_date: '2013-02-02' + + expect(response).to have_http_status(201) + expect(json_response['description']).to eq('release') + expect(json_response['due_date']).to eq('2013-03-02') + expect(json_response['start_date']).to eq('2013-02-02') + end + + it 'returns a 400 error if title is missing' do + post v3_api("/projects/#{project.id}/milestones", user) + + expect(response).to have_http_status(400) + end + + it 'returns a 400 error if params are invalid (duplicate title)' do + post v3_api("/projects/#{project.id}/milestones", user), + title: milestone.title, description: 'release', due_date: '2013-03-02' + + expect(response).to have_http_status(400) + end + + it 'creates a new project with reserved html characters' do + post v3_api("/projects/#{project.id}/milestones", user), title: 'foo & bar 1.1 -> 2.2' + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq('foo & bar 1.1 -> 2.2') + expect(json_response['description']).to be_nil + end + end + + describe 'PUT /projects/:id/milestones/:milestone_id' do + it 'updates a project milestone' do + put v3_api("/projects/#{project.id}/milestones/#{milestone.id}", user), + title: 'updated title' + + expect(response).to have_http_status(200) + expect(json_response['title']).to eq('updated title') + end + + it 'removes a due date if nil is passed' do + milestone.update!(due_date: "2016-08-05") + + put v3_api("/projects/#{project.id}/milestones/#{milestone.id}", user), due_date: nil + + expect(response).to have_http_status(200) + expect(json_response['due_date']).to be_nil + end + + it 'returns a 404 error if milestone id not found' do + put v3_api("/projects/#{project.id}/milestones/1234", user), + title: 'updated title' + + expect(response).to have_http_status(404) + end + end + + describe 'PUT /projects/:id/milestones/:milestone_id to close milestone' do + it 'updates a project milestone' do + put v3_api("/projects/#{project.id}/milestones/#{milestone.id}", user), + state_event: 'close' + expect(response).to have_http_status(200) + + expect(json_response['state']).to eq('closed') + end + end + + describe 'PUT /projects/:id/milestones/:milestone_id to test observer on close' do + it 'creates an activity event when an milestone is closed' do + expect(Event).to receive(:create) + + put v3_api("/projects/#{project.id}/milestones/#{milestone.id}", user), + state_event: 'close' + end + end + + describe 'GET /projects/:id/milestones/:milestone_id/issues' do + before do + milestone.issues << create(:issue, project: project) + end + it 'returns project issues for a particular milestone' do + get v3_api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['milestone']['title']).to eq(milestone.title) + end + + it 'returns a 401 error if user not authenticated' do + get v3_api("/projects/#{project.id}/milestones/#{milestone.id}/issues") + + expect(response).to have_http_status(401) + end + + describe 'confidential issues' do + let(:public_project) { create(:empty_project, :public) } + let(:milestone) { create(:milestone, project: public_project) } + let(:issue) { create(:issue, project: public_project) } + let(:confidential_issue) { create(:issue, confidential: true, project: public_project) } + + before do + public_project.team << [user, :developer] + milestone.issues << issue << confidential_issue + end + + it 'returns confidential issues to team members' do + get v3_api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(2) + expect(json_response.map { |issue| issue['id'] }).to include(issue.id, confidential_issue.id) + end + + it 'does not return confidential issues to team members with guest role' do + member = create(:user) + project.team << [member, :guest] + + get v3_api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", member) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + expect(json_response.map { |issue| issue['id'] }).to include(issue.id) + end + + it 'does not return confidential issues to regular users' do + get v3_api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", create(:user)) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + expect(json_response.map { |issue| issue['id'] }).to include(issue.id) + end + end + end +end diff --git a/spec/requests/api/v3/notes_spec.rb b/spec/requests/api/v3/notes_spec.rb new file mode 100644 index 00000000000..ddef2d5eb04 --- /dev/null +++ b/spec/requests/api/v3/notes_spec.rb @@ -0,0 +1,433 @@ +require 'spec_helper' + +describe API::V3::Notes, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let!(:project) { create(:empty_project, :public, namespace: user.namespace) } + let!(:issue) { create(:issue, project: project, author: user) } + let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) } + let!(:snippet) { create(:project_snippet, project: project, author: user) } + let!(:issue_note) { create(:note, noteable: issue, project: project, author: user) } + let!(:merge_request_note) { create(:note, noteable: merge_request, project: project, author: user) } + let!(:snippet_note) { create(:note, noteable: snippet, project: project, author: user) } + + # For testing the cross-reference of a private issue in a public issue + let(:private_user) { create(:user) } + let(:private_project) do + create(:empty_project, namespace: private_user.namespace). + tap { |p| p.team << [private_user, :master] } + end + let(:private_issue) { create(:issue, project: private_project) } + + let(:ext_proj) { create(:empty_project, :public) } + let(:ext_issue) { create(:issue, project: ext_proj) } + + let!(:cross_reference_note) do + create :note, + noteable: ext_issue, project: ext_proj, + note: "mentioned in issue #{private_issue.to_reference(ext_proj)}", + system: true + end + + before { project.team << [user, :reporter] } + + describe "GET /projects/:id/noteable/:noteable_id/notes" do + context "when noteable is an Issue" do + it "returns an array of issue notes" do + get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first['body']).to eq(issue_note.note) + expect(json_response.first['upvote']).to be_falsey + expect(json_response.first['downvote']).to be_falsey + end + + it "returns a 404 error when issue id not found" do + get v3_api("/projects/#{project.id}/issues/12345/notes", user) + + expect(response).to have_http_status(404) + end + + context "and current user cannot view the notes" do + it "returns an empty array" do + get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response).to be_empty + end + + context "and issue is confidential" do + before { ext_issue.update_attributes(confidential: true) } + + it "returns 404" do + get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user) + + expect(response).to have_http_status(404) + end + end + + context "and current user can view the note" do + it "returns an empty array" do + get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", private_user) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first['body']).to eq(cross_reference_note.note) + end + end + end + end + + context "when noteable is a Snippet" do + it "returns an array of snippet notes" do + get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first['body']).to eq(snippet_note.note) + end + + it "returns a 404 error when snippet id not found" do + get v3_api("/projects/#{project.id}/snippets/42/notes", user) + + expect(response).to have_http_status(404) + end + + it "returns 404 when not authorized" do + get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes", private_user) + + expect(response).to have_http_status(404) + end + end + + context "when noteable is a Merge Request" do + it "returns an array of merge_requests notes" do + get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes", user) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first['body']).to eq(merge_request_note.note) + end + + it "returns a 404 error if merge request id not found" do + get v3_api("/projects/#{project.id}/merge_requests/4444/notes", user) + + expect(response).to have_http_status(404) + end + + it "returns 404 when not authorized" do + get v3_api("/projects/#{project.id}/merge_requests/4444/notes", private_user) + + expect(response).to have_http_status(404) + end + end + end + + describe "GET /projects/:id/noteable/:noteable_id/notes/:note_id" do + context "when noteable is an Issue" do + it "returns an issue note by id" do + get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['body']).to eq(issue_note.note) + end + + it "returns a 404 error if issue note not found" do + get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user) + + expect(response).to have_http_status(404) + end + + context "and current user cannot view the note" do + it "returns a 404 error" do + get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", user) + + expect(response).to have_http_status(404) + end + + context "when issue is confidential" do + before { issue.update_attributes(confidential: true) } + + it "returns 404" do + get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", private_user) + + expect(response).to have_http_status(404) + end + end + + context "and current user can view the note" do + it "returns an issue note by id" do + get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", private_user) + + expect(response).to have_http_status(200) + expect(json_response['body']).to eq(cross_reference_note.note) + end + end + end + end + + context "when noteable is a Snippet" do + it "returns a snippet note by id" do + get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes/#{snippet_note.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['body']).to eq(snippet_note.note) + end + + it "returns a 404 error if snippet note not found" do + get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes/12345", user) + + expect(response).to have_http_status(404) + end + end + end + + describe "POST /projects/:id/noteable/:noteable_id/notes" do + context "when noteable is an Issue" do + it "creates a new issue note" do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!' + + expect(response).to have_http_status(201) + expect(json_response['body']).to eq('hi!') + expect(json_response['author']['username']).to eq(user.username) + end + + it "returns a 400 bad request error if body not given" do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user) + + expect(response).to have_http_status(400) + end + + it "returns a 401 unauthorized error if user not authenticated" do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes"), body: 'hi!' + + expect(response).to have_http_status(401) + end + + context 'when an admin or owner makes the request' do + it 'accepts the creation date to be set' do + creation_time = 2.weeks.ago + post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user), + body: 'hi!', created_at: creation_time + + expect(response).to have_http_status(201) + expect(json_response['body']).to eq('hi!') + expect(json_response['author']['username']).to eq(user.username) + expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time) + end + end + + context 'when the user is posting an award emoji on an issue created by someone else' do + let(:issue2) { create(:issue, project: project) } + + it 'creates a new issue note' do + post v3_api("/projects/#{project.id}/issues/#{issue2.id}/notes", user), body: ':+1:' + + expect(response).to have_http_status(201) + expect(json_response['body']).to eq(':+1:') + end + end + + context 'when the user is posting an award emoji on his/her own issue' do + it 'creates a new issue note' do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: ':+1:' + + expect(response).to have_http_status(201) + expect(json_response['body']).to eq(':+1:') + end + end + end + + context "when noteable is a Snippet" do + it "creates a new snippet note" do + post v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user), body: 'hi!' + + expect(response).to have_http_status(201) + expect(json_response['body']).to eq('hi!') + expect(json_response['author']['username']).to eq(user.username) + end + + it "returns a 400 bad request error if body not given" do + post v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user) + + expect(response).to have_http_status(400) + end + + it "returns a 401 unauthorized error if user not authenticated" do + post v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes"), body: 'hi!' + + expect(response).to have_http_status(401) + end + end + + context 'when user does not have access to read the noteable' do + it 'responds with 404' do + project = create(:empty_project, :private) { |p| p.add_guest(user) } + issue = create(:issue, :confidential, project: project) + + post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user), + body: 'Foo' + + expect(response).to have_http_status(404) + end + end + + context 'when user does not have access to create noteable' do + let(:private_issue) { create(:issue, project: create(:empty_project, :private)) } + + ## + # We are posting to project user has access to, but we use issue id + # from a different project, see #15577 + # + before do + post v3_api("/projects/#{project.id}/issues/#{private_issue.id}/notes", user), + body: 'Hi!' + end + + it 'responds with resource not found error' do + expect(response.status).to eq 404 + end + + it 'does not create new note' do + expect(private_issue.notes.reload).to be_empty + end + end + end + + describe "POST /projects/:id/noteable/:noteable_id/notes to test observer on create" do + it "creates an activity event when an issue note is created" do + expect(Event).to receive(:create) + + post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!' + end + end + + describe 'PUT /projects/:id/noteable/:noteable_id/notes/:note_id' do + context 'when noteable is an Issue' do + it 'returns modified note' do + put v3_api("/projects/#{project.id}/issues/#{issue.id}/"\ + "notes/#{issue_note.id}", user), body: 'Hello!' + + expect(response).to have_http_status(200) + expect(json_response['body']).to eq('Hello!') + end + + it 'returns a 404 error when note id not found' do + put v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user), + body: 'Hello!' + + expect(response).to have_http_status(404) + end + + it 'returns a 400 bad request error if body not given' do + put v3_api("/projects/#{project.id}/issues/#{issue.id}/"\ + "notes/#{issue_note.id}", user) + + expect(response).to have_http_status(400) + end + end + + context 'when noteable is a Snippet' do + it 'returns modified note' do + put v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\ + "notes/#{snippet_note.id}", user), body: 'Hello!' + + expect(response).to have_http_status(200) + expect(json_response['body']).to eq('Hello!') + end + + it 'returns a 404 error when note id not found' do + put v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\ + "notes/12345", user), body: "Hello!" + + expect(response).to have_http_status(404) + end + end + + context 'when noteable is a Merge Request' do + it 'returns modified note' do + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\ + "notes/#{merge_request_note.id}", user), body: 'Hello!' + + expect(response).to have_http_status(200) + expect(json_response['body']).to eq('Hello!') + end + + it 'returns a 404 error when note id not found' do + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\ + "notes/12345", user), body: "Hello!" + + expect(response).to have_http_status(404) + end + end + end + + describe 'DELETE /projects/:id/noteable/:noteable_id/notes/:note_id' do + context 'when noteable is an Issue' do + it 'deletes a note' do + delete v3_api("/projects/#{project.id}/issues/#{issue.id}/"\ + "notes/#{issue_note.id}", user) + + expect(response).to have_http_status(200) + # Check if note is really deleted + delete v3_api("/projects/#{project.id}/issues/#{issue.id}/"\ + "notes/#{issue_note.id}", user) + expect(response).to have_http_status(404) + end + + it 'returns a 404 error when note id not found' do + delete v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user) + + expect(response).to have_http_status(404) + end + end + + context 'when noteable is a Snippet' do + it 'deletes a note' do + delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\ + "notes/#{snippet_note.id}", user) + + expect(response).to have_http_status(200) + # Check if note is really deleted + delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\ + "notes/#{snippet_note.id}", user) + expect(response).to have_http_status(404) + end + + it 'returns a 404 error when note id not found' do + delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\ + "notes/12345", user) + + expect(response).to have_http_status(404) + end + end + + context 'when noteable is a Merge Request' do + it 'deletes a note' do + delete v3_api("/projects/#{project.id}/merge_requests/"\ + "#{merge_request.id}/notes/#{merge_request_note.id}", user) + + expect(response).to have_http_status(200) + # Check if note is really deleted + delete v3_api("/projects/#{project.id}/merge_requests/"\ + "#{merge_request.id}/notes/#{merge_request_note.id}", user) + expect(response).to have_http_status(404) + end + + it 'returns a 404 error when note id not found' do + delete v3_api("/projects/#{project.id}/merge_requests/"\ + "#{merge_request.id}/notes/12345", user) + + expect(response).to have_http_status(404) + end + end + end +end diff --git a/spec/requests/api/v3/pipelines_spec.rb b/spec/requests/api/v3/pipelines_spec.rb new file mode 100644 index 00000000000..3786eb06932 --- /dev/null +++ b/spec/requests/api/v3/pipelines_spec.rb @@ -0,0 +1,203 @@ +require 'spec_helper' + +describe API::V3::Pipelines, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:non_member) { create(:user) } + let(:project) { create(:project, :repository, creator: user) } + + let!(:pipeline) do + create(:ci_empty_pipeline, project: project, sha: project.commit.id, + ref: project.default_branch) + end + + before { project.team << [user, :master] } + + shared_examples 'a paginated resources' do + before do + # Fires the request + request + end + + it 'has pagination headers' do + expect(response).to include_pagination_headers + end + end + + describe 'GET /projects/:id/pipelines ' do + it_behaves_like 'a paginated resources' do + let(:request) { get v3_api("/projects/#{project.id}/pipelines", user) } + end + + context 'authorized user' do + it 'returns project pipelines' do + get v3_api("/projects/#{project.id}/pipelines", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['sha']).to match(/\A\h{40}\z/) + expect(json_response.first['id']).to eq pipeline.id + expect(json_response.first.keys).to contain_exactly(*%w[id sha ref status before_sha tag yaml_errors user created_at updated_at started_at finished_at committed_at duration coverage]) + end + end + + context 'unauthorized user' do + it 'does not return project pipelines' do + get v3_api("/projects/#{project.id}/pipelines", non_member) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq '404 Project Not Found' + expect(json_response).not_to be_an Array + end + end + end + + describe 'POST /projects/:id/pipeline ' do + context 'authorized user' do + context 'with gitlab-ci.yml' do + before { stub_ci_pipeline_to_return_yaml_file } + + it 'creates and returns a new pipeline' do + expect do + post v3_api("/projects/#{project.id}/pipeline", user), ref: project.default_branch + end.to change { Ci::Pipeline.count }.by(1) + + expect(response).to have_http_status(201) + expect(json_response).to be_a Hash + expect(json_response['sha']).to eq project.commit.id + end + + it 'fails when using an invalid ref' do + post v3_api("/projects/#{project.id}/pipeline", user), ref: 'invalid_ref' + + expect(response).to have_http_status(400) + expect(json_response['message']['base'].first).to eq 'Reference not found' + expect(json_response).not_to be_an Array + end + end + + context 'without gitlab-ci.yml' do + it 'fails to create pipeline' do + post v3_api("/projects/#{project.id}/pipeline", user), ref: project.default_branch + + expect(response).to have_http_status(400) + expect(json_response['message']['base'].first).to eq 'Missing .gitlab-ci.yml file' + expect(json_response).not_to be_an Array + end + end + end + + context 'unauthorized user' do + it 'does not create pipeline' do + post v3_api("/projects/#{project.id}/pipeline", non_member), ref: project.default_branch + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq '404 Project Not Found' + expect(json_response).not_to be_an Array + end + end + end + + describe 'GET /projects/:id/pipelines/:pipeline_id' do + context 'authorized user' do + it 'returns project pipelines' do + get v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['sha']).to match /\A\h{40}\z/ + end + + it 'returns 404 when it does not exist' do + get v3_api("/projects/#{project.id}/pipelines/123456", user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq '404 Not found' + expect(json_response['id']).to be nil + end + + context 'with coverage' do + before do + create(:ci_build, coverage: 30, pipeline: pipeline) + end + + it 'exposes the coverage' do + get v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}", user) + + expect(json_response["coverage"].to_i).to eq(30) + end + end + end + + context 'unauthorized user' do + it 'should not return a project pipeline' do + get v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}", non_member) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq '404 Project Not Found' + expect(json_response['id']).to be nil + end + end + end + + describe 'POST /projects/:id/pipelines/:pipeline_id/retry' do + context 'authorized user' do + let!(:pipeline) do + create(:ci_pipeline, project: project, sha: project.commit.id, + ref: project.default_branch) + end + + let!(:build) { create(:ci_build, :failed, pipeline: pipeline) } + + it 'retries failed builds' do + expect do + post v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", user) + end.to change { pipeline.builds.count }.from(1).to(2) + + expect(response).to have_http_status(201) + expect(build.reload.retried?).to be true + end + end + + context 'unauthorized user' do + it 'should not return a project pipeline' do + post v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", non_member) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq '404 Project Not Found' + expect(json_response['id']).to be nil + end + end + end + + describe 'POST /projects/:id/pipelines/:pipeline_id/cancel' do + let!(:pipeline) do + create(:ci_empty_pipeline, project: project, sha: project.commit.id, + ref: project.default_branch) + end + + let!(:build) { create(:ci_build, :running, pipeline: pipeline) } + + context 'authorized user' do + it 'retries failed builds' do + post v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", user) + + expect(response).to have_http_status(200) + expect(json_response['status']).to eq('canceled') + end + end + + context 'user without proper access rights' do + let!(:reporter) { create(:user) } + + before { project.team << [reporter, :reporter] } + + it 'rejects the action' do + post v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", reporter) + + expect(response).to have_http_status(403) + expect(pipeline.reload.status).to eq('pending') + end + end + end +end diff --git a/spec/requests/api/v3/project_snippets_spec.rb b/spec/requests/api/v3/project_snippets_spec.rb index 3700477f0db..957a3bf97ef 100644 --- a/spec/requests/api/v3/project_snippets_spec.rb +++ b/spec/requests/api/v3/project_snippets_spec.rb @@ -85,43 +85,33 @@ describe API::ProjectSnippets, api: true do allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) end - context 'when the project is private' do - let(:private_project) { create(:project_empty_repo, :private) } - - context 'when the snippet is public' do - it 'creates the snippet' do - expect { create_snippet(private_project, visibility_level: Snippet::PUBLIC) }. - to change { Snippet.count }.by(1) - end + context 'when the snippet is private' do + it 'creates the snippet' do + expect { create_snippet(project, visibility_level: Snippet::PRIVATE) }. + to change { Snippet.count }.by(1) end end - context 'when the project is public' do - context 'when the snippet is private' do - it 'creates the snippet' do - expect { create_snippet(project, visibility_level: Snippet::PRIVATE) }. - to change { Snippet.count }.by(1) - end + context 'when the snippet is public' do + it 'rejects the shippet' do + expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }. + not_to change { Snippet.count } + + expect(response).to have_http_status(400) + expect(json_response['message']).to eq({ "error" => "Spam detected" }) end - context 'when the snippet is public' do - it 'rejects the shippet' do - expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }. - not_to change { Snippet.count } - expect(response).to have_http_status(400) - end - - it 'creates a spam log' do - expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }. - to change { SpamLog.count }.by(1) - end + it 'creates a spam log' do + expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }. + to change { SpamLog.count }.by(1) end end end end describe 'PUT /projects/:project_id/snippets/:id/' do - let(:snippet) { create(:project_snippet, author: admin) } + let(:visibility_level) { Snippet::PUBLIC } + let(:snippet) { create(:project_snippet, author: admin, visibility_level: visibility_level) } it 'updates snippet' do new_content = 'New content' @@ -145,6 +135,56 @@ describe API::ProjectSnippets, api: true do expect(response).to have_http_status(400) end + + context 'when the snippet is spam' do + def update_snippet(snippet_params = {}) + put v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}", admin), snippet_params + end + + before do + allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) + end + + context 'when the snippet is private' do + let(:visibility_level) { Snippet::PRIVATE } + + it 'creates the snippet' do + expect { update_snippet(title: 'Foo') }. + to change { snippet.reload.title }.to('Foo') + end + end + + context 'when the snippet is public' do + let(:visibility_level) { Snippet::PUBLIC } + + it 'rejects the snippet' do + expect { update_snippet(title: 'Foo') }. + not_to change { snippet.reload.title } + end + + it 'creates a spam log' do + expect { update_snippet(title: 'Foo') }. + to change { SpamLog.count }.by(1) + end + end + + context 'when the private snippet is made public' do + let(:visibility_level) { Snippet::PRIVATE } + + it 'rejects the snippet' do + expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }. + not_to change { snippet.reload.title } + + expect(response).to have_http_status(400) + expect(json_response['message']).to eq({ "error" => "Spam detected" }) + end + + it 'creates a spam log' do + expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }. + to change { SpamLog.count }.by(1) + end + end + end end describe 'DELETE /projects/:project_id/snippets/:id/' do diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb index a495122bba7..d8bb562587d 100644 --- a/spec/requests/api/v3/projects_spec.rb +++ b/spec/requests/api/v3/projects_spec.rb @@ -84,7 +84,7 @@ describe API::V3::Projects, api: true do context 'GET /projects?simple=true' do it 'returns a simplified version of all the projects' do - expected_keys = ["id", "http_url_to_repo", "web_url", "name", "name_with_namespace", "path", "path_with_namespace"] + expected_keys = %w(id http_url_to_repo web_url name name_with_namespace path path_with_namespace) get v3_api('/projects?simple=true', user) @@ -309,10 +309,37 @@ describe API::V3::Projects, api: true do end end - it 'creates new project without path and return 201' do - expect { post v3_api('/projects', user), name: 'foo' }. + it 'creates new project without path but with name and returns 201' do + expect { post v3_api('/projects', user), name: 'Foo Project' }. to change { Project.count }.by(1) expect(response).to have_http_status(201) + + project = Project.first + + expect(project.name).to eq('Foo Project') + expect(project.path).to eq('foo-project') + end + + it 'creates new project without name but with path and returns 201' do + expect { post v3_api('/projects', user), path: 'foo_project' }. + to change { Project.count }.by(1) + expect(response).to have_http_status(201) + + project = Project.first + + expect(project.name).to eq('foo_project') + expect(project.path).to eq('foo_project') + end + + it 'creates new project name and path and returns 201' do + expect { post v3_api('/projects', user), path: 'foo-Project', name: 'Foo Project' }. + to change { Project.count }.by(1) + expect(response).to have_http_status(201) + + project = Project.first + + expect(project.name).to eq('Foo Project') + expect(project.path).to eq('foo-Project') end it 'creates last project before reaching project limit' do @@ -321,7 +348,7 @@ describe API::V3::Projects, api: true do expect(response).to have_http_status(201) end - it 'does not create new project without name and return 400' do + it 'does not create new project without name or path and return 400' do expect { post v3_api('/projects', user) }.not_to change { Project.count } expect(response).to have_http_status(400) end @@ -400,7 +427,7 @@ describe API::V3::Projects, api: true do expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey end - it 'sets a project as allowing merge only if build succeeds' do + it 'sets a project as allowing merge only if merge_when_pipeline_succeeds' do project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true }) post v3_api('/projects', user), project expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy @@ -545,7 +572,7 @@ describe API::V3::Projects, api: true do expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey end - it 'sets a project as allowing merge only if build succeeds' do + it 'sets a project as allowing merge only if merge_when_pipeline_succeeds' do project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true }) post v3_api("/projects/user/#{user.id}", admin), project expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy @@ -642,7 +669,7 @@ describe API::V3::Projects, api: true do expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id) expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name) expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access) - expect(json_response['only_allow_merge_if_build_succeeds']).to eq(project.only_allow_merge_if_build_succeeds) + expect(json_response['only_allow_merge_if_build_succeeds']).to eq(project.only_allow_merge_if_pipeline_succeeds) expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved) end @@ -682,6 +709,7 @@ describe API::V3::Projects, api: true do 'name' => user.namespace.name, 'path' => user.namespace.path, 'kind' => user.namespace.kind, + 'full_path' => user.namespace.full_path, }) end diff --git a/spec/requests/api/v3/repositories_spec.rb b/spec/requests/api/v3/repositories_spec.rb new file mode 100644 index 00000000000..c696721c1c9 --- /dev/null +++ b/spec/requests/api/v3/repositories_spec.rb @@ -0,0 +1,144 @@ +require 'spec_helper' +require 'mime/types' + +describe API::V3::Repositories, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:guest) { create(:user).tap { |u| create(:project_member, :guest, user: u, project: project) } } + let!(:project) { create(:project, :repository, creator: user) } + let!(:master) { create(:project_member, :master, user: user, project: project) } + + describe "GET /projects/:id/repository/tree" do + let(:route) { "/projects/#{project.id}/repository/tree" } + + shared_examples_for 'repository tree' do + it 'returns the repository tree' do + get v3_api(route, current_user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + + first_commit = json_response.first + expect(first_commit['name']).to eq('bar') + expect(first_commit['type']).to eq('tree') + expect(first_commit['mode']).to eq('040000') + end + + context 'when ref does not exist' do + it_behaves_like '404 response' do + let(:request) { get v3_api("#{route}?ref_name=foo", current_user) } + let(:message) { '404 Tree Not Found' } + end + end + + context 'when repository is disabled' do + include_context 'disabled repository' + + it_behaves_like '403 response' do + let(:request) { get v3_api(route, current_user) } + end + end + + context 'with recursive=1' do + it 'returns recursive project paths tree' do + get v3_api("#{route}?recursive=1", current_user) + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response[4]['name']).to eq('html') + expect(json_response[4]['path']).to eq('files/html') + expect(json_response[4]['type']).to eq('tree') + expect(json_response[4]['mode']).to eq('040000') + end + + context 'when repository is disabled' do + include_context 'disabled repository' + + it_behaves_like '403 response' do + let(:request) { get v3_api(route, current_user) } + end + end + + context 'when ref does not exist' do + it_behaves_like '404 response' do + let(:request) { get v3_api("#{route}?recursive=1&ref_name=foo", current_user) } + let(:message) { '404 Tree Not Found' } + end + end + end + end + + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository tree' do + let(:project) { create(:project, :public, :repository) } + let(:current_user) { nil } + end + end + + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get v3_api(route) } + let(:message) { '404 Project Not Found' } + end + end + + context 'when authenticated', 'as a developer' do + it_behaves_like 'repository tree' do + let(:current_user) { user } + end + end + + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get v3_api(route, guest) } + end + end + end + + describe 'GET /projects/:id/repository/contributors' do + let(:route) { "/projects/#{project.id}/repository/contributors" } + + shared_examples_for 'repository contributors' do + it 'returns valid data' do + get v3_api(route, current_user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + + first_contributor = json_response.first + expect(first_contributor['email']).to eq('tiagonbotelho@hotmail.com') + expect(first_contributor['name']).to eq('tiagonbotelho') + expect(first_contributor['commits']).to eq(1) + expect(first_contributor['additions']).to eq(0) + expect(first_contributor['deletions']).to eq(0) + end + end + + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository contributors' do + let(:project) { create(:project, :public, :repository) } + let(:current_user) { nil } + end + end + + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get v3_api(route) } + let(:message) { '404 Project Not Found' } + end + end + + context 'when authenticated', 'as a developer' do + it_behaves_like 'repository contributors' do + let(:current_user) { user } + end + end + + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get v3_api(route, guest) } + end + end + end +end diff --git a/spec/requests/api/v3/runners_spec.rb b/spec/requests/api/v3/runners_spec.rb new file mode 100644 index 00000000000..ca335ce9cf0 --- /dev/null +++ b/spec/requests/api/v3/runners_spec.rb @@ -0,0 +1,154 @@ +require 'spec_helper' + +describe API::V3::Runners, api: true do + include ApiHelpers + + let(:admin) { create(:user, :admin) } + let(:user) { create(:user) } + let(:user2) { create(:user) } + + let(:project) { create(:empty_project, creator_id: user.id) } + let(:project2) { create(:empty_project, creator_id: user.id) } + + let!(:shared_runner) { create(:ci_runner, :shared) } + let!(:unused_specific_runner) { create(:ci_runner) } + + let!(:specific_runner) do + create(:ci_runner).tap do |runner| + create(:ci_runner_project, runner: runner, project: project) + end + end + + let!(:two_projects_runner) do + create(:ci_runner).tap do |runner| + create(:ci_runner_project, runner: runner, project: project) + create(:ci_runner_project, runner: runner, project: project2) + end + end + + before do + # Set project access for users + create(:project_member, :master, user: user, project: project) + create(:project_member, :reporter, user: user2, project: project) + end + + describe 'DELETE /runners/:id' do + context 'admin user' do + context 'when runner is shared' do + it 'deletes runner' do + expect do + delete v3_api("/runners/#{shared_runner.id}", admin) + + expect(response).to have_http_status(200) + end.to change{ Ci::Runner.shared.count }.by(-1) + end + end + + context 'when runner is not shared' do + it 'deletes unused runner' do + expect do + delete v3_api("/runners/#{unused_specific_runner.id}", admin) + + expect(response).to have_http_status(200) + end.to change{ Ci::Runner.specific.count }.by(-1) + end + + it 'deletes used runner' do + expect do + delete v3_api("/runners/#{specific_runner.id}", admin) + + expect(response).to have_http_status(200) + end.to change{ Ci::Runner.specific.count }.by(-1) + end + end + + it 'returns 404 if runner does not exists' do + delete v3_api('/runners/9999', admin) + + expect(response).to have_http_status(404) + end + end + + context 'authorized user' do + context 'when runner is shared' do + it 'does not delete runner' do + delete v3_api("/runners/#{shared_runner.id}", user) + expect(response).to have_http_status(403) + end + end + + context 'when runner is not shared' do + it 'does not delete runner without access to it' do + delete v3_api("/runners/#{specific_runner.id}", user2) + expect(response).to have_http_status(403) + end + + it 'does not delete runner with more than one associated project' do + delete v3_api("/runners/#{two_projects_runner.id}", user) + expect(response).to have_http_status(403) + end + + it 'deletes runner for one owned project' do + expect do + delete v3_api("/runners/#{specific_runner.id}", user) + + expect(response).to have_http_status(200) + end.to change{ Ci::Runner.specific.count }.by(-1) + end + end + end + + context 'unauthorized user' do + it 'does not delete runner' do + delete v3_api("/runners/#{specific_runner.id}") + + expect(response).to have_http_status(401) + end + end + end + + describe 'DELETE /projects/:id/runners/:runner_id' do + context 'authorized user' do + context 'when runner have more than one associated projects' do + it "disables project's runner" do + expect do + delete v3_api("/projects/#{project.id}/runners/#{two_projects_runner.id}", user) + + expect(response).to have_http_status(200) + end.to change{ project.runners.count }.by(-1) + end + end + + context 'when runner have one associated projects' do + it "does not disable project's runner" do + expect do + delete v3_api("/projects/#{project.id}/runners/#{specific_runner.id}", user) + end.to change{ project.runners.count }.by(0) + expect(response).to have_http_status(403) + end + end + + it 'returns 404 is runner is not found' do + delete v3_api("/projects/#{project.id}/runners/9999", user) + + expect(response).to have_http_status(404) + end + end + + context 'authorized user without permissions' do + it "does not disable project's runner" do + delete v3_api("/projects/#{project.id}/runners/#{specific_runner.id}", user2) + + expect(response).to have_http_status(403) + end + end + + context 'unauthorized user' do + it "does not disable project's runner" do + delete v3_api("/projects/#{project.id}/runners/#{specific_runner.id}") + + expect(response).to have_http_status(401) + end + end + end +end diff --git a/spec/requests/api/v3/services_spec.rb b/spec/requests/api/v3/services_spec.rb new file mode 100644 index 00000000000..7e8c8753d02 --- /dev/null +++ b/spec/requests/api/v3/services_spec.rb @@ -0,0 +1,22 @@ +require "spec_helper" + +describe API::V3::Services, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) } + + Service.available_services_names.each do |service| + describe "DELETE /projects/:id/services/#{service.dasherize}" do + include_context service + + it "deletes #{service}" do + delete v3_api("/projects/#{project.id}/services/#{dashed_service}", user) + + expect(response).to have_http_status(200) + project.send(service_method).reload + expect(project.send(service_method).activated?).to be_falsey + end + end + end +end diff --git a/spec/requests/api/v3/settings_spec.rb b/spec/requests/api/v3/settings_spec.rb new file mode 100644 index 00000000000..a9fa5adac17 --- /dev/null +++ b/spec/requests/api/v3/settings_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +describe API::V3::Settings, 'Settings', api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:admin) { create(:admin) } + + describe "GET /application/settings" do + it "returns application settings" do + get v3_api("/application/settings", admin) + expect(response).to have_http_status(200) + expect(json_response).to be_an Hash + expect(json_response['default_projects_limit']).to eq(42) + expect(json_response['signin_enabled']).to be_truthy + expect(json_response['repository_storage']).to eq('default') + expect(json_response['koding_enabled']).to be_falsey + expect(json_response['koding_url']).to be_nil + expect(json_response['plantuml_enabled']).to be_falsey + expect(json_response['plantuml_url']).to be_nil + end + end + + describe "PUT /application/settings" do + context "custom repository storage type set in the config" do + before do + storages = { 'custom' => 'tmp/tests/custom_repositories' } + allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) + end + + it "updates application settings" do + put v3_api("/application/settings", admin), + default_projects_limit: 3, signin_enabled: false, repository_storage: 'custom', koding_enabled: true, koding_url: 'http://koding.example.com', + plantuml_enabled: true, plantuml_url: 'http://plantuml.example.com' + expect(response).to have_http_status(200) + expect(json_response['default_projects_limit']).to eq(3) + expect(json_response['signin_enabled']).to be_falsey + expect(json_response['repository_storage']).to eq('custom') + expect(json_response['repository_storages']).to eq(['custom']) + expect(json_response['koding_enabled']).to be_truthy + expect(json_response['koding_url']).to eq('http://koding.example.com') + expect(json_response['plantuml_enabled']).to be_truthy + expect(json_response['plantuml_url']).to eq('http://plantuml.example.com') + end + end + + context "missing koding_url value when koding_enabled is true" do + it "returns a blank parameter error message" do + put v3_api("/application/settings", admin), koding_enabled: true + + expect(response).to have_http_status(400) + expect(json_response['error']).to eq('koding_url is missing') + end + end + + context "missing plantuml_url value when plantuml_enabled is true" do + it "returns a blank parameter error message" do + put v3_api("/application/settings", admin), plantuml_enabled: true + + expect(response).to have_http_status(400) + expect(json_response['error']).to eq('plantuml_url is missing') + end + end + end +end diff --git a/spec/requests/api/v3/snippets_spec.rb b/spec/requests/api/v3/snippets_spec.rb new file mode 100644 index 00000000000..05653bd0d51 --- /dev/null +++ b/spec/requests/api/v3/snippets_spec.rb @@ -0,0 +1,187 @@ +require 'rails_helper' + +describe API::V3::Snippets, api: true do + include ApiHelpers + let!(:user) { create(:user) } + + describe 'GET /snippets/' do + it 'returns snippets available' do + public_snippet = create(:personal_snippet, :public, author: user) + private_snippet = create(:personal_snippet, :private, author: user) + internal_snippet = create(:personal_snippet, :internal, author: user) + + get v3_api("/snippets/", user) + + expect(response).to have_http_status(200) + expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly( + public_snippet.id, + internal_snippet.id, + private_snippet.id) + expect(json_response.last).to have_key('web_url') + expect(json_response.last).to have_key('raw_url') + end + + it 'hides private snippets from regular user' do + create(:personal_snippet, :private) + + get v3_api("/snippets/", user) + expect(response).to have_http_status(200) + expect(json_response.size).to eq(0) + end + end + + describe 'GET /snippets/public' do + let!(:other_user) { create(:user) } + let!(:public_snippet) { create(:personal_snippet, :public, author: user) } + let!(:private_snippet) { create(:personal_snippet, :private, author: user) } + let!(:internal_snippet) { create(:personal_snippet, :internal, author: user) } + let!(:public_snippet_other) { create(:personal_snippet, :public, author: other_user) } + let!(:private_snippet_other) { create(:personal_snippet, :private, author: other_user) } + let!(:internal_snippet_other) { create(:personal_snippet, :internal, author: other_user) } + + it 'returns all snippets with public visibility from all users' do + get v3_api("/snippets/public", user) + + expect(response).to have_http_status(200) + expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly( + public_snippet.id, + public_snippet_other.id) + expect(json_response.map{ |snippet| snippet['web_url']} ).to include( + "http://localhost/snippets/#{public_snippet.id}", + "http://localhost/snippets/#{public_snippet_other.id}") + expect(json_response.map{ |snippet| snippet['raw_url']} ).to include( + "http://localhost/snippets/#{public_snippet.id}/raw", + "http://localhost/snippets/#{public_snippet_other.id}/raw") + end + end + + describe 'GET /snippets/:id/raw' do + let(:snippet) { create(:personal_snippet, author: user) } + + it 'returns raw text' do + get v3_api("/snippets/#{snippet.id}/raw", user) + + expect(response).to have_http_status(200) + expect(response.content_type).to eq 'text/plain' + expect(response.body).to eq(snippet.content) + end + + it 'returns 404 for invalid snippet id' do + delete v3_api("/snippets/1234", user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Snippet Not Found') + end + end + + describe 'POST /snippets/' do + let(:params) do + { + title: 'Test Title', + file_name: 'test.rb', + content: 'puts "hello world"', + visibility_level: Snippet::PUBLIC + } + end + + it 'creates a new snippet' do + expect do + post v3_api("/snippets/", user), params + end.to change { PersonalSnippet.count }.by(1) + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq(params[:title]) + expect(json_response['file_name']).to eq(params[:file_name]) + end + + it 'returns 400 for missing parameters' do + params.delete(:title) + + post v3_api("/snippets/", user), params + + expect(response).to have_http_status(400) + end + + context 'when the snippet is spam' do + def create_snippet(snippet_params = {}) + post v3_api('/snippets', user), params.merge(snippet_params) + end + + before do + allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) + end + + context 'when the snippet is private' do + it 'creates the snippet' do + expect { create_snippet(visibility_level: Snippet::PRIVATE) }. + to change { Snippet.count }.by(1) + end + end + + context 'when the snippet is public' do + it 'rejects the shippet' do + expect { create_snippet(visibility_level: Snippet::PUBLIC) }. + not_to change { Snippet.count } + expect(response).to have_http_status(400) + end + + it 'creates a spam log' do + expect { create_snippet(visibility_level: Snippet::PUBLIC) }. + to change { SpamLog.count }.by(1) + end + end + end + end + + describe 'PUT /snippets/:id' do + let(:other_user) { create(:user) } + let(:public_snippet) { create(:personal_snippet, :public, author: user) } + it 'updates snippet' do + new_content = 'New content' + + put v3_api("/snippets/#{public_snippet.id}", user), content: new_content + + expect(response).to have_http_status(200) + public_snippet.reload + expect(public_snippet.content).to eq(new_content) + end + + it 'returns 404 for invalid snippet id' do + put v3_api("/snippets/1234", user), title: 'foo' + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Snippet Not Found') + end + + it "returns 404 for another user's snippet" do + put v3_api("/snippets/#{public_snippet.id}", other_user), title: 'fubar' + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Snippet Not Found') + end + + it 'returns 400 for missing parameters' do + put v3_api("/snippets/1234", user) + + expect(response).to have_http_status(400) + end + end + + describe 'DELETE /snippets/:id' do + let!(:public_snippet) { create(:personal_snippet, :public, author: user) } + it 'deletes snippet' do + expect do + delete v3_api("/snippets/#{public_snippet.id}", user) + + expect(response).to have_http_status(204) + end.to change { PersonalSnippet.count }.by(-1) + end + + it 'returns 404 for invalid snippet id' do + delete v3_api("/snippets/1234", user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Snippet Not Found') + end + end +end diff --git a/spec/requests/api/v3/system_hooks_spec.rb b/spec/requests/api/v3/system_hooks_spec.rb new file mode 100644 index 00000000000..91038977c82 --- /dev/null +++ b/spec/requests/api/v3/system_hooks_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' + +describe API::V3::SystemHooks, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:admin) { create(:admin) } + let!(:hook) { create(:system_hook, url: "http://example.com") } + + before { stub_request(:post, hook.url) } + + describe "GET /hooks" do + context "when no user" do + it "returns authentication error" do + get v3_api("/hooks") + + expect(response).to have_http_status(401) + end + end + + context "when not an admin" do + it "returns forbidden error" do + get v3_api("/hooks", user) + + expect(response).to have_http_status(403) + end + end + + context "when authenticated as admin" do + it "returns an array of hooks" do + get v3_api("/hooks", admin) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['url']).to eq(hook.url) + expect(json_response.first['push_events']).to be true + expect(json_response.first['tag_push_events']).to be false + end + end + end + + describe "DELETE /hooks/:id" do + it "deletes a hook" do + expect do + delete v3_api("/hooks/#{hook.id}", admin) + + expect(response).to have_http_status(200) + end.to change { SystemHook.count }.by(-1) + end + + it 'returns 404 if the system hook does not exist' do + delete v3_api('/hooks/12345', admin) + + expect(response).to have_http_status(404) + end + end +end diff --git a/spec/requests/api/v3/tags_spec.rb b/spec/requests/api/v3/tags_spec.rb new file mode 100644 index 00000000000..6870cfd2668 --- /dev/null +++ b/spec/requests/api/v3/tags_spec.rb @@ -0,0 +1,89 @@ +require 'spec_helper' +require 'mime/types' + +describe API::V3::Tags, api: true do + include ApiHelpers + include RepoHelpers + + let(:user) { create(:user) } + let(:user2) { create(:user) } + let!(:project) { create(:project, :repository, creator: user) } + let!(:master) { create(:project_member, :master, user: user, project: project) } + + describe "GET /projects/:id/repository/tags" do + let(:tag_name) { project.repository.tag_names.sort.reverse.first } + let(:description) { 'Awesome release!' } + + shared_examples_for 'repository tags' do + it 'returns the repository tags' do + get v3_api("/projects/#{project.id}/repository/tags", current_user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(tag_name) + end + end + + context 'when unauthenticated' do + it_behaves_like 'repository tags' do + let(:project) { create(:project, :public, :repository) } + let(:current_user) { nil } + end + end + + context 'when authenticated' do + it_behaves_like 'repository tags' do + let(:current_user) { user } + end + end + + context 'without releases' do + it "returns an array of project tags" do + get v3_api("/projects/#{project.id}/repository/tags", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(tag_name) + end + end + + context 'with releases' do + before do + release = project.releases.find_or_initialize_by(tag: tag_name) + release.update_attributes(description: description) + end + + it "returns an array of project tags with release info" do + get v3_api("/projects/#{project.id}/repository/tags", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(tag_name) + expect(json_response.first['message']).to eq('Version 1.1.0') + expect(json_response.first['release']['description']).to eq(description) + end + end + end + + describe 'DELETE /projects/:id/repository/tags/:tag_name' do + let(:tag_name) { project.repository.tag_names.sort.reverse.first } + + before do + allow_any_instance_of(Repository).to receive(:rm_tag).and_return(true) + end + + context 'delete tag' do + it 'deletes an existing tag' do + delete v3_api("/projects/#{project.id}/repository/tags/#{tag_name}", user) + + expect(response).to have_http_status(200) + expect(json_response['tag_name']).to eq(tag_name) + end + + it 'raises 404 if the tag does not exist' do + delete v3_api("/projects/#{project.id}/repository/tags/foobar", user) + expect(response).to have_http_status(404) + end + end + end +end diff --git a/spec/requests/api/v3/templates_spec.rb b/spec/requests/api/v3/templates_spec.rb new file mode 100644 index 00000000000..f1e554b98cc --- /dev/null +++ b/spec/requests/api/v3/templates_spec.rb @@ -0,0 +1,203 @@ +require 'spec_helper' + +describe API::V3::Templates, api: true do + include ApiHelpers + + shared_examples_for 'the Template Entity' do |path| + before { get v3_api(path) } + + it { expect(json_response['name']).to eq('Ruby') } + it { expect(json_response['content']).to include('*.gem') } + end + + shared_examples_for 'the TemplateList Entity' do |path| + before { get v3_api(path) } + + it { expect(json_response.first['name']).not_to be_nil } + it { expect(json_response.first['content']).to be_nil } + end + + shared_examples_for 'requesting gitignores' do |path| + it 'returns a list of available gitignore templates' do + get v3_api(path) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to be > 15 + end + end + + shared_examples_for 'requesting gitlab-ci-ymls' do |path| + it 'returns a list of available gitlab_ci_ymls' do + get v3_api(path) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).not_to be_nil + end + end + + shared_examples_for 'requesting gitlab-ci-yml for Ruby' do |path| + it 'adds a disclaimer on the top' do + get v3_api(path) + + expect(response).to have_http_status(200) + expect(json_response['content']).to start_with("# This file is a template,") + end + end + + shared_examples_for 'the License Template Entity' do |path| + before { get v3_api(path) } + + it 'returns a license template' do + expect(json_response['key']).to eq('mit') + expect(json_response['name']).to eq('MIT License') + expect(json_response['nickname']).to be_nil + expect(json_response['popular']).to be true + expect(json_response['html_url']).to eq('http://choosealicense.com/licenses/mit/') + expect(json_response['source_url']).to eq('https://opensource.org/licenses/MIT') + expect(json_response['description']).to include('A short and simple permissive license with conditions') + expect(json_response['conditions']).to eq(%w[include-copyright]) + expect(json_response['permissions']).to eq(%w[commercial-use modifications distribution private-use]) + expect(json_response['limitations']).to eq(%w[no-liability]) + expect(json_response['content']).to include('MIT License') + end + end + + shared_examples_for 'GET licenses' do |path| + it 'returns a list of available license templates' do + get v3_api(path) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(12) + expect(json_response.map { |l| l['key'] }).to include('agpl-3.0') + end + + describe 'the popular parameter' do + context 'with popular=1' do + it 'returns a list of available popular license templates' do + get v3_api("#{path}?popular=1") + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(3) + expect(json_response.map { |l| l['key'] }).to include('apache-2.0') + end + end + end + end + + shared_examples_for 'GET licenses/:name' do |path| + context 'with :project and :fullname given' do + before do + get v3_api("#{path}/#{license_type}?project=My+Awesome+Project&fullname=Anton+#{license_type.upcase}") + end + + context 'for the mit license' do + let(:license_type) { 'mit' } + + it 'returns the license text' do + expect(json_response['content']).to include('MIT License') + end + + it 'replaces placeholder values' do + expect(json_response['content']).to include("Copyright (c) #{Time.now.year} Anton") + end + end + + context 'for the agpl-3.0 license' do + let(:license_type) { 'agpl-3.0' } + + it 'returns the license text' do + expect(json_response['content']).to include('GNU AFFERO GENERAL PUBLIC LICENSE') + end + + it 'replaces placeholder values' do + expect(json_response['content']).to include('My Awesome Project') + expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton") + end + end + + context 'for the gpl-3.0 license' do + let(:license_type) { 'gpl-3.0' } + + it 'returns the license text' do + expect(json_response['content']).to include('GNU GENERAL PUBLIC LICENSE') + end + + it 'replaces placeholder values' do + expect(json_response['content']).to include('My Awesome Project') + expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton") + end + end + + context 'for the gpl-2.0 license' do + let(:license_type) { 'gpl-2.0' } + + it 'returns the license text' do + expect(json_response['content']).to include('GNU GENERAL PUBLIC LICENSE') + end + + it 'replaces placeholder values' do + expect(json_response['content']).to include('My Awesome Project') + expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton") + end + end + + context 'for the apache-2.0 license' do + let(:license_type) { 'apache-2.0' } + + it 'returns the license text' do + expect(json_response['content']).to include('Apache License') + end + + it 'replaces placeholder values' do + expect(json_response['content']).to include("Copyright #{Time.now.year} Anton") + end + end + + context 'for an uknown license' do + let(:license_type) { 'muth-over9000' } + + it 'returns a 404' do + expect(response).to have_http_status(404) + end + end + end + + context 'with no :fullname given' do + context 'with an authenticated user' do + let(:user) { create(:user) } + + it 'replaces the copyright owner placeholder with the name of the current user' do + get v3_api('/templates/licenses/mit', user) + + expect(json_response['content']).to include("Copyright (c) #{Time.now.year} #{user.name}") + end + end + end + end + + describe 'with /templates namespace' do + it_behaves_like 'the Template Entity', '/templates/gitignores/Ruby' + it_behaves_like 'the TemplateList Entity', '/templates/gitignores' + it_behaves_like 'requesting gitignores', '/templates/gitignores' + it_behaves_like 'requesting gitlab-ci-ymls', '/templates/gitlab_ci_ymls' + it_behaves_like 'requesting gitlab-ci-yml for Ruby', '/templates/gitlab_ci_ymls/Ruby' + it_behaves_like 'the License Template Entity', '/templates/licenses/mit' + it_behaves_like 'GET licenses', '/templates/licenses' + it_behaves_like 'GET licenses/:name', '/templates/licenses' + end + + describe 'without /templates namespace' do + it_behaves_like 'the Template Entity', '/gitignores/Ruby' + it_behaves_like 'the TemplateList Entity', '/gitignores' + it_behaves_like 'requesting gitignores', '/gitignores' + it_behaves_like 'requesting gitlab-ci-ymls', '/gitlab_ci_ymls' + it_behaves_like 'requesting gitlab-ci-yml for Ruby', '/gitlab_ci_ymls/Ruby' + it_behaves_like 'the License Template Entity', '/licenses/mit' + it_behaves_like 'GET licenses', '/licenses' + it_behaves_like 'GET licenses/:name', '/licenses' + end +end diff --git a/spec/requests/api/v3/todos_spec.rb b/spec/requests/api/v3/todos_spec.rb new file mode 100644 index 00000000000..80fa697e949 --- /dev/null +++ b/spec/requests/api/v3/todos_spec.rb @@ -0,0 +1,73 @@ +require 'spec_helper' + +describe API::V3::Todos, api: true do + include ApiHelpers + + let(:project_1) { create(:empty_project) } + let(:project_2) { create(:empty_project) } + let(:author_1) { create(:user) } + let(:author_2) { create(:user) } + let(:john_doe) { create(:user, username: 'john_doe') } + let!(:pending_1) { create(:todo, :mentioned, project: project_1, author: author_1, user: john_doe) } + let!(:pending_2) { create(:todo, project: project_2, author: author_2, user: john_doe) } + let!(:pending_3) { create(:todo, project: project_1, author: author_2, user: john_doe) } + let!(:done) { create(:todo, :done, project: project_1, author: author_1, user: john_doe) } + + before do + project_1.team << [john_doe, :developer] + project_2.team << [john_doe, :developer] + end + + describe 'DELETE /todos/:id' do + context 'when unauthenticated' do + it 'returns authentication error' do + delete v3_api("/todos/#{pending_1.id}") + + expect(response.status).to eq(401) + end + end + + context 'when authenticated' do + it 'marks a todo as done' do + delete v3_api("/todos/#{pending_1.id}", john_doe) + + expect(response.status).to eq(200) + expect(pending_1.reload).to be_done + end + + it 'updates todos cache' do + expect_any_instance_of(User).to receive(:update_todos_count_cache).and_call_original + + delete v3_api("/todos/#{pending_1.id}", john_doe) + end + end + end + + describe 'DELETE /todos' do + context 'when unauthenticated' do + it 'returns authentication error' do + delete v3_api('/todos') + + expect(response.status).to eq(401) + end + end + + context 'when authenticated' do + it 'marks all todos as done' do + delete v3_api('/todos', john_doe) + + expect(response.status).to eq(200) + expect(response.body).to eq('3') + expect(pending_1.reload).to be_done + expect(pending_2.reload).to be_done + expect(pending_3.reload).to be_done + end + + it 'updates todos cache' do + expect_any_instance_of(User).to receive(:update_todos_count_cache).and_call_original + + delete v3_api("/todos", john_doe) + end + end + end +end diff --git a/spec/requests/api/v3/triggers_spec.rb b/spec/requests/api/v3/triggers_spec.rb new file mode 100644 index 00000000000..721ce4a361b --- /dev/null +++ b/spec/requests/api/v3/triggers_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe API::V3::Triggers do + include ApiHelpers + + let(:user) { create(:user) } + let(:user2) { create(:user) } + let!(:trigger_token) { 'secure_token' } + let!(:project) { create(:project, :repository, creator: user) } + let!(:master) { create(:project_member, :master, user: user, project: project) } + let!(:developer) { create(:project_member, :developer, user: user2, project: project) } + let!(:trigger) { create(:ci_trigger, project: project, token: trigger_token) } + + describe 'DELETE /projects/:id/triggers/:token' do + context 'authenticated user with valid permissions' do + it 'deletes trigger' do + expect do + delete v3_api("/projects/#{project.id}/triggers/#{trigger.token}", user) + + expect(response).to have_http_status(200) + end.to change{project.triggers.count}.by(-1) + end + + it 'responds with 404 Not Found if requesting non-existing trigger' do + delete v3_api("/projects/#{project.id}/triggers/abcdef012345", user) + + expect(response).to have_http_status(404) + end + end + + context 'authenticated user with invalid permissions' do + it 'does not delete trigger' do + delete v3_api("/projects/#{project.id}/triggers/#{trigger.token}", user2) + + expect(response).to have_http_status(403) + end + end + + context 'unauthenticated user' do + it 'does not delete trigger' do + delete v3_api("/projects/#{project.id}/triggers/#{trigger.token}") + + expect(response).to have_http_status(401) + end + end + end +end diff --git a/spec/requests/api/v3/users_spec.rb b/spec/requests/api/v3/users_spec.rb new file mode 100644 index 00000000000..17bbb0b53c1 --- /dev/null +++ b/spec/requests/api/v3/users_spec.rb @@ -0,0 +1,266 @@ +require 'spec_helper' + +describe API::V3::Users, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:admin) { create(:admin) } + let(:key) { create(:key, user: user) } + let(:email) { create(:email, user: user) } + let(:ldap_blocked_user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') } + + describe 'GET /user/:id/keys' do + before { admin } + + context 'when unauthenticated' do + it 'returns authentication error' do + get v3_api("/users/#{user.id}/keys") + expect(response).to have_http_status(401) + end + end + + context 'when authenticated' do + it 'returns 404 for non-existing user' do + get v3_api('/users/999999/keys', admin) + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 User Not Found') + end + + it 'returns array of ssh keys' do + user.keys << key + user.save + + get v3_api("/users/#{user.id}/keys", admin) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['title']).to eq(key.title) + end + end + end + + describe 'GET /user/:id/emails' do + before { admin } + + context 'when unauthenticated' do + it 'returns authentication error' do + get v3_api("/users/#{user.id}/emails") + expect(response).to have_http_status(401) + end + end + + context 'when authenticated' do + it 'returns 404 for non-existing user' do + get v3_api('/users/999999/emails', admin) + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 User Not Found') + end + + it 'returns array of emails' do + user.emails << email + user.save + + get v3_api("/users/#{user.id}/emails", admin) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['email']).to eq(email.email) + end + + it "returns a 404 for invalid ID" do + put v3_api("/users/ASDF/emails", admin) + + expect(response).to have_http_status(404) + end + end + end + + describe "GET /user/keys" do + context "when unauthenticated" do + it "returns authentication error" do + get v3_api("/user/keys") + expect(response).to have_http_status(401) + end + end + + context "when authenticated" do + it "returns array of ssh keys" do + user.keys << key + user.save + + get v3_api("/user/keys", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first["title"]).to eq(key.title) + end + end + end + + describe "GET /user/emails" do + context "when unauthenticated" do + it "returns authentication error" do + get v3_api("/user/emails") + expect(response).to have_http_status(401) + end + end + + context "when authenticated" do + it "returns array of emails" do + user.emails << email + user.save + + get v3_api("/user/emails", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first["email"]).to eq(email.email) + end + end + end + + describe 'PUT /users/:id/block' do + before { admin } + it 'blocks existing user' do + put v3_api("/users/#{user.id}/block", admin) + expect(response).to have_http_status(200) + expect(user.reload.state).to eq('blocked') + end + + it 'does not re-block ldap blocked users' do + put v3_api("/users/#{ldap_blocked_user.id}/block", admin) + expect(response).to have_http_status(403) + expect(ldap_blocked_user.reload.state).to eq('ldap_blocked') + end + + it 'does not be available for non admin users' do + put v3_api("/users/#{user.id}/block", user) + expect(response).to have_http_status(403) + expect(user.reload.state).to eq('active') + end + + it 'returns a 404 error if user id not found' do + put v3_api('/users/9999/block', admin) + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 User Not Found') + end + end + + describe 'PUT /users/:id/unblock' do + let(:blocked_user) { create(:user, state: 'blocked') } + before { admin } + + it 'unblocks existing user' do + put v3_api("/users/#{user.id}/unblock", admin) + expect(response).to have_http_status(200) + expect(user.reload.state).to eq('active') + end + + it 'unblocks a blocked user' do + put v3_api("/users/#{blocked_user.id}/unblock", admin) + expect(response).to have_http_status(200) + expect(blocked_user.reload.state).to eq('active') + end + + it 'does not unblock ldap blocked users' do + put v3_api("/users/#{ldap_blocked_user.id}/unblock", admin) + expect(response).to have_http_status(403) + expect(ldap_blocked_user.reload.state).to eq('ldap_blocked') + end + + it 'does not be available for non admin users' do + put v3_api("/users/#{user.id}/unblock", user) + expect(response).to have_http_status(403) + expect(user.reload.state).to eq('active') + end + + it 'returns a 404 error if user id not found' do + put v3_api('/users/9999/block', admin) + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 User Not Found') + end + + it "returns a 404 for invalid ID" do + put v3_api("/users/ASDF/block", admin) + + expect(response).to have_http_status(404) + end + end + + describe 'GET /users/:id/events' do + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + let(:note) { create(:note_on_issue, note: 'What an awesome day!', project: project) } + + before do + project.add_user(user, :developer) + EventCreateService.new.leave_note(note, user) + end + + context "as a user than cannot see the event's project" do + it 'returns no events' do + other_user = create(:user) + + get api("/users/#{user.id}/events", other_user) + + expect(response).to have_http_status(200) + expect(json_response).to be_empty + end + end + + context "as a user than can see the event's project" do + context 'joined event' do + it 'returns the "joined" event' do + get v3_api("/users/#{user.id}/events", user) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + + comment_event = json_response.find { |e| e['action_name'] == 'commented on' } + + expect(comment_event['project_id'].to_i).to eq(project.id) + expect(comment_event['author_username']).to eq(user.username) + expect(comment_event['note']['id']).to eq(note.id) + expect(comment_event['note']['body']).to eq('What an awesome day!') + + joined_event = json_response.find { |e| e['action_name'] == 'joined' } + + expect(joined_event['project_id'].to_i).to eq(project.id) + expect(joined_event['author_username']).to eq(user.username) + expect(joined_event['author']['name']).to eq(user.name) + end + end + + context 'when there are multiple events from different projects' do + let(:second_note) { create(:note_on_issue, project: create(:empty_project)) } + let(:third_note) { create(:note_on_issue, project: project) } + + before do + second_note.project.add_user(user, :developer) + + [second_note, third_note].each do |note| + EventCreateService.new.leave_note(note, user) + end + end + + it 'returns events in the correct order (from newest to oldest)' do + get v3_api("/users/#{user.id}/events", user) + + comment_events = json_response.select { |e| e['action_name'] == 'commented on' } + + expect(comment_events[0]['target_id']).to eq(third_note.id) + expect(comment_events[1]['target_id']).to eq(second_note.id) + expect(comment_events[2]['target_id']).to eq(note.id) + end + end + end + + it 'returns a 404 error if not found' do + get v3_api('/users/42/events', user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 User Not Found') + end + end +end diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb index 769f04c5057..0c1413119e0 100644 --- a/spec/requests/api/variables_spec.rb +++ b/spec/requests/api/variables_spec.rb @@ -152,8 +152,9 @@ describe API::Variables, api: true do it 'deletes variable' do expect do delete api("/projects/#{project.id}/variables/#{variable.key}", user) + + expect(response).to have_http_status(204) end.to change{project.variables.count}.by(-1) - expect(response).to have_http_status(200) end it 'responds with 404 Not Found if requesting non-existing variable' do diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index d85afdeab42..9948d1a9ea0 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Ci::API::Builds do include ApiHelpers - let(:runner) { FactoryGirl.create(:ci_runner, tag_list: ["mysql", "ruby"]) } + let(:runner) { FactoryGirl.create(:ci_runner, tag_list: %w(mysql ruby)) } let(:project) { FactoryGirl.create(:empty_project, shared_runners_enabled: false) } let(:last_update) { nil } @@ -630,6 +630,7 @@ describe Ci::API::Builds do context 'with an expire date' do let!(:artifacts) { file_upload } + let(:default_artifacts_expire_in) {} let(:post_data) do { 'file.path' => artifacts.path, @@ -638,6 +639,9 @@ describe Ci::API::Builds do end before do + stub_application_setting( + default_artifacts_expire_in: default_artifacts_expire_in) + post(post_url, post_data, headers_with_token) end @@ -648,7 +652,8 @@ describe Ci::API::Builds do build.reload expect(response).to have_http_status(201) expect(json_response['artifacts_expire_at']).not_to be_empty - expect(build.artifacts_expire_at).to be_within(5.minutes).of(Time.now + 7.days) + expect(build.artifacts_expire_at). + to be_within(5.minutes).of(7.days.from_now) end end @@ -661,6 +666,32 @@ describe Ci::API::Builds do expect(json_response['artifacts_expire_at']).to be_nil expect(build.artifacts_expire_at).to be_nil end + + context 'with application default' do + context 'default to 5 days' do + let(:default_artifacts_expire_in) { '5 days' } + + it 'sets to application default' do + build.reload + expect(response).to have_http_status(201) + expect(json_response['artifacts_expire_at']) + .not_to be_empty + expect(build.artifacts_expire_at) + .to be_within(5.minutes).of(5.days.from_now) + end + end + + context 'default to 0' do + let(:default_artifacts_expire_in) { '0' } + + it 'does not set expire_in' do + build.reload + expect(response).to have_http_status(201) + expect(json_response['artifacts_expire_at']).to be_nil + expect(build.artifacts_expire_at).to be_nil + end + end + end end end diff --git a/spec/requests/ci/api/runners_spec.rb b/spec/requests/ci/api/runners_spec.rb index bd55934d0c8..8719313783e 100644 --- a/spec/requests/ci/api/runners_spec.rb +++ b/spec/requests/ci/api/runners_spec.rb @@ -41,7 +41,7 @@ describe Ci::API::Runners do it 'creates runner' do expect(response).to have_http_status 201 - expect(Ci::Runner.first.tag_list.sort).to eq(["tag1", "tag2"]) + expect(Ci::Runner.first.tag_list.sort).to eq(%w(tag1 tag2)) end end diff --git a/spec/requests/ci/api/triggers_spec.rb b/spec/requests/ci/api/triggers_spec.rb index a30be767119..5321f8b134f 100644 --- a/spec/requests/ci/api/triggers_spec.rb +++ b/spec/requests/ci/api/triggers_spec.rb @@ -60,7 +60,8 @@ describe Ci::API::Triggers do it 'validates variables to be a hash' do post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options.merge(variables: 'value') expect(response).to have_http_status(400) - expect(json_response['message']).to eq('variables needs to be a hash') + + expect(json_response['error']).to eq('variables is invalid') end it 'validates variables needs to be a map of key-valued strings' do diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb index c0e7bab8199..5d495bc9e7d 100644 --- a/spec/requests/lfs_http_spec.rb +++ b/spec/requests/lfs_http_spec.rb @@ -25,11 +25,9 @@ describe 'Git LFS API and storage' do { 'objects' => [ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', - 'size' => 1575078 - }, + 'size' => 1575078 }, { 'oid' => sample_oid, - 'size' => sample_size - } + 'size' => sample_size } ], 'operation' => 'upload' } @@ -53,11 +51,9 @@ describe 'Git LFS API and storage' do { 'objects' => [ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', - 'size' => 1575078 - }, + 'size' => 1575078 }, { 'oid' => sample_oid, - 'size' => sample_size - } + 'size' => sample_size } ], 'operation' => 'upload' } @@ -374,11 +370,12 @@ describe 'Git LFS API and storage' do describe 'download' do let(:project) { create(:empty_project) } let(:body) do - { 'operation' => 'download', + { + 'operation' => 'download', 'objects' => [ { 'oid' => sample_oid, - 'size' => sample_size - }] + 'size' => sample_size } + ] } end @@ -393,16 +390,20 @@ describe 'Git LFS API and storage' do end it 'with href to download' do - expect(json_response).to eq('objects' => [ - { 'oid' => sample_oid, - 'size' => sample_size, - 'actions' => { - 'download' => { - 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", - 'header' => { 'Authorization' => authorization } + expect(json_response).to eq({ + 'objects' => [ + { + 'oid' => sample_oid, + 'size' => sample_size, + 'actions' => { + 'download' => { + 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", + 'header' => { 'Authorization' => authorization } + } } } - }]) + ] + }) end end @@ -417,24 +418,29 @@ describe 'Git LFS API and storage' do end it 'with href to download' do - expect(json_response).to eq('objects' => [ - { 'oid' => sample_oid, - 'size' => sample_size, - 'error' => { - 'code' => 404, - 'message' => "Object does not exist on the server or you don't have permissions to access it", + expect(json_response).to eq({ + 'objects' => [ + { + 'oid' => sample_oid, + 'size' => sample_size, + 'error' => { + 'code' => 404, + 'message' => "Object does not exist on the server or you don't have permissions to access it", + } } - }]) + ] + }) end end context 'when downloading a lfs object that does not exist' do let(:body) do - { 'operation' => 'download', + { + 'operation' => 'download', 'objects' => [ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', - 'size' => 1575078 - }] + 'size' => 1575078 } + ] } end @@ -443,27 +449,30 @@ describe 'Git LFS API and storage' do end it 'with an 404 for specific object' do - expect(json_response).to eq('objects' => [ - { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', - 'size' => 1575078, - 'error' => { - 'code' => 404, - 'message' => "Object does not exist on the server or you don't have permissions to access it", + expect(json_response).to eq({ + 'objects' => [ + { + 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', + 'size' => 1575078, + 'error' => { + 'code' => 404, + 'message' => "Object does not exist on the server or you don't have permissions to access it", + } } - }]) + ] + }) end end context 'when downloading one new and one existing lfs object' do let(:body) do - { 'operation' => 'download', + { + 'operation' => 'download', 'objects' => [ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', - 'size' => 1575078 - }, + 'size' => 1575078 }, { 'oid' => sample_oid, - 'size' => sample_size - } + 'size' => sample_size } ] } end @@ -477,23 +486,28 @@ describe 'Git LFS API and storage' do end it 'responds with upload hypermedia link for the new object' do - expect(json_response).to eq('objects' => [ - { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', - 'size' => 1575078, - 'error' => { - 'code' => 404, - 'message' => "Object does not exist on the server or you don't have permissions to access it", - } - }, - { 'oid' => sample_oid, - 'size' => sample_size, - 'actions' => { - 'download' => { - 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", - 'header' => { 'Authorization' => authorization } + expect(json_response).to eq({ + 'objects' => [ + { + 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', + 'size' => 1575078, + 'error' => { + 'code' => 404, + 'message' => "Object does not exist on the server or you don't have permissions to access it", + } + }, + { + 'oid' => sample_oid, + 'size' => sample_size, + 'actions' => { + 'download' => { + 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", + 'header' => { 'Authorization' => authorization } + } } } - }]) + ] + }) end end end @@ -597,17 +611,21 @@ describe 'Git LFS API and storage' do end it 'responds with status 200 and href to download' do - expect(json_response).to eq('objects' => [ - { 'oid' => sample_oid, - 'size' => sample_size, - 'authenticated' => true, - 'actions' => { - 'download' => { - 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", - 'header' => {} + expect(json_response).to eq({ + 'objects' => [ + { + 'oid' => sample_oid, + 'size' => sample_size, + 'authenticated' => true, + 'actions' => { + 'download' => { + 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", + 'header' => {} + } } } - }]) + ] + }) end end @@ -626,11 +644,12 @@ describe 'Git LFS API and storage' do describe 'upload' do let(:project) { create(:project, :public) } let(:body) do - { 'operation' => 'upload', + { + 'operation' => 'upload', 'objects' => [ { 'oid' => sample_oid, - 'size' => sample_size - }] + 'size' => sample_size } + ] } end @@ -665,11 +684,12 @@ describe 'Git LFS API and storage' do context 'when pushing a lfs object that does not exist' do let(:body) do - { 'operation' => 'upload', + { + 'operation' => 'upload', 'objects' => [ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', - 'size' => 1575078 - }] + 'size' => 1575078 } + ] } end @@ -688,14 +708,13 @@ describe 'Git LFS API and storage' do context 'when pushing one new and one existing lfs object' do let(:body) do - { 'operation' => 'upload', + { + 'operation' => 'upload', 'objects' => [ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', - 'size' => 1575078 - }, + 'size' => 1575078 }, { 'oid' => sample_oid, - 'size' => sample_size - } + 'size' => sample_size } ] } end @@ -789,11 +808,12 @@ describe 'Git LFS API and storage' do let(:project) { create(:empty_project) } let(:authorization) { authorize_user } let(:body) do - { 'operation' => 'other', + { + 'operation' => 'other', 'objects' => [ { 'oid' => sample_oid, - 'size' => sample_size - }] + 'size' => sample_size } + ] } end diff --git a/spec/rubocop/cop/custom_error_class_spec.rb b/spec/rubocop/cop/custom_error_class_spec.rb new file mode 100644 index 00000000000..381d7871a40 --- /dev/null +++ b/spec/rubocop/cop/custom_error_class_spec.rb @@ -0,0 +1,111 @@ +require 'spec_helper' + +require 'rubocop' +require 'rubocop/rspec/support' + +require_relative '../../../rubocop/cop/custom_error_class' + +describe RuboCop::Cop::CustomErrorClass do + include CopHelper + + subject(:cop) { described_class.new } + + context 'when a class has a body' do + it 'does nothing' do + inspect_source(cop, 'class CustomError < StandardError; def foo; end; end') + + expect(cop.offenses).to be_empty + end + end + + context 'when a class has no explicit superclass' do + it 'does nothing' do + inspect_source(cop, 'class CustomError; end') + + expect(cop.offenses).to be_empty + end + end + + context 'when a class has a superclass that does not end in Error' do + it 'does nothing' do + inspect_source(cop, 'class CustomError < BasicObject; end') + + expect(cop.offenses).to be_empty + end + end + + context 'when a class is empty and inherits from a class ending in Error' do + context 'when the class is on a single line' do + let(:source) do + <<-SOURCE + module Foo + class CustomError < Bar::Baz::BaseError; end + end + SOURCE + end + + let(:expected) do + <<-EXPECTED + module Foo + CustomError = Class.new(Bar::Baz::BaseError) + end + EXPECTED + end + + it 'registers an offense' do + expected_highlights = source.split("\n")[1].strip + + inspect_source(cop, source) + + aggregate_failures do + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.map(&:line)).to eq([2]) + expect(cop.highlights).to contain_exactly(expected_highlights) + end + end + + it 'autocorrects to the right version' do + autocorrected = autocorrect_source(cop, source, 'foo/custom_error.rb') + + expect(autocorrected).to eq(expected) + end + end + + context 'when the class is on multiple lines' do + let(:source) do + <<-SOURCE + module Foo + class CustomError < Bar::Baz::BaseError + end + end + SOURCE + end + + let(:expected) do + <<-EXPECTED + module Foo + CustomError = Class.new(Bar::Baz::BaseError) + end + EXPECTED + end + + it 'registers an offense' do + expected_highlights = source.split("\n")[1..2].join("\n").strip + + inspect_source(cop, source) + + aggregate_failures do + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.map(&:line)).to eq([2]) + expect(cop.highlights).to contain_exactly(expected_highlights) + end + end + + it 'autocorrects to the right version' do + autocorrected = autocorrect_source(cop, source, 'foo/custom_error.rb') + + expect(autocorrected).to eq(expected) + end + end + end +end diff --git a/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb b/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb new file mode 100644 index 00000000000..7cb24dc5646 --- /dev/null +++ b/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' +require 'rubocop' +require 'rubocop/rspec/support' +require_relative '../../../../rubocop/cop/migration/add_concurrent_foreign_key' + +describe RuboCop::Cop::Migration::AddConcurrentForeignKey do + include CopHelper + + let(:cop) { described_class.new } + + context 'outside of a migration' do + it 'does not register any offenses' do + inspect_source(cop, 'def up; add_foreign_key(:projects, :users, column: :user_id); end') + + expect(cop.offenses).to be_empty + end + end + + context 'in a migration' do + before do + allow(cop).to receive(:in_migration?).and_return(true) + end + + it 'registers an offense when using add_foreign_key' do + inspect_source(cop, 'def up; add_foreign_key(:projects, :users, column: :user_id); end') + + aggregate_failures do + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.map(&:line)).to eq([1]) + end + end + end +end diff --git a/spec/serializers/environment_serializer_spec.rb b/spec/serializers/environment_serializer_spec.rb index 1b95f1ff198..6a6df377b35 100644 --- a/spec/serializers/environment_serializer_spec.rb +++ b/spec/serializers/environment_serializer_spec.rb @@ -181,6 +181,17 @@ describe EnvironmentSerializer do expect(subject.first[:name]).to eq 'production' expect(subject.second[:name]).to eq 'staging' end + + it 'appends correct total page count header' do + expect(subject).not_to be_empty + expect(response).to have_received(:[]=).with('X-Total', '3') + end + + it 'appends correct page count headers' do + expect(subject).not_to be_empty + expect(response).to have_received(:[]=).with('X-Total-Pages', '2') + expect(response).to have_received(:[]=).with('X-Per-Page', '2') + end end end end diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index bb26513103d..b91234ddb1e 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -72,7 +72,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do shared_examples 'a pullable and pushable' do it_behaves_like 'a accessible' do - let(:actions) { ['pull', 'push'] } + let(:actions) { %w(pull push) } end end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index ceaca96e25b..8459a3d8cfb 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -79,66 +79,53 @@ describe Ci::CreatePipelineService, services: true do context 'when commit contains a [ci skip] directive' do let(:message) { "some message[ci skip]" } - let(:messageFlip) { "some message[skip ci]" } - let(:capMessage) { "some message[CI SKIP]" } - let(:capMessageFlip) { "some message[SKIP CI]" } + + ci_messages = [ + "some message[ci skip]", + "some message[skip ci]", + "some message[CI SKIP]", + "some message[SKIP CI]", + "some message[ci_skip]", + "some message[skip_ci]", + "some message[ci-skip]", + "some message[skip-ci]" + ] before do allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message } end - it "skips builds creation if there is [ci skip] tag in commit message" do - commits = [{ message: message }] - pipeline = execute(ref: 'refs/heads/master', - before: '00000000', - after: project.commit.id, - commits: commits) + ci_messages.each do |ci_message| + it "skips builds creation if the commit message is #{ci_message}" do + commits = [{ message: ci_message }] + pipeline = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: commits) - expect(pipeline).to be_persisted - expect(pipeline.builds.any?).to be false - expect(pipeline.status).to eq("skipped") - end - - it "skips builds creation if there is [skip ci] tag in commit message" do - commits = [{ message: messageFlip }] - pipeline = execute(ref: 'refs/heads/master', - before: '00000000', - after: project.commit.id, - commits: commits) - - expect(pipeline).to be_persisted - expect(pipeline.builds.any?).to be false - expect(pipeline.status).to eq("skipped") + expect(pipeline).to be_persisted + expect(pipeline.builds.any?).to be false + expect(pipeline.status).to eq("skipped") + end end - it "skips builds creation if there is [CI SKIP] tag in commit message" do - commits = [{ message: capMessage }] - pipeline = execute(ref: 'refs/heads/master', - before: '00000000', - after: project.commit.id, - commits: commits) - - expect(pipeline).to be_persisted - expect(pipeline.builds.any?).to be false - expect(pipeline.status).to eq("skipped") - end + it "does not skips builds creation if there is no [ci skip] or [skip ci] tag in commit message" do + allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { "some message" } - it "skips builds creation if there is [SKIP CI] tag in commit message" do - commits = [{ message: capMessageFlip }] + commits = [{ message: "some message" }] pipeline = execute(ref: 'refs/heads/master', before: '00000000', after: project.commit.id, commits: commits) expect(pipeline).to be_persisted - expect(pipeline.builds.any?).to be false - expect(pipeline.status).to eq("skipped") + expect(pipeline.builds.first.name).to eq("rspec") end - it "does not skips builds creation if there is no [ci skip] or [skip ci] tag in commit message" do - allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { "some message" } + it "does not skip builds creation if the commit message is nil" do + allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { nil } - commits = [{ message: "some message" }] + commits = [{ message: nil }] pipeline = execute(ref: 'refs/heads/master', before: '00000000', after: project.commit.id, diff --git a/spec/services/ci/image_for_build_service_spec.rb b/spec/services/ci/image_for_build_service_spec.rb deleted file mode 100644 index b3e0a7b9b58..00000000000 --- a/spec/services/ci/image_for_build_service_spec.rb +++ /dev/null @@ -1,50 +0,0 @@ -require 'spec_helper' - -module Ci - describe ImageForBuildService, services: true do - let(:service) { ImageForBuildService.new } - let(:project) { FactoryGirl.create(:empty_project) } - let(:commit_sha) { '01234567890123456789' } - let(:pipeline) { project.ensure_pipeline('master', commit_sha) } - let(:build) { FactoryGirl.create(:ci_build, pipeline: pipeline) } - - describe '#execute' do - before { build } - - context 'branch name' do - before { allow(project).to receive(:commit).and_return(OpenStruct.new(sha: commit_sha)) } - before { build.run! } - let(:image) { service.execute(project, ref: 'master') } - - it { expect(image).to be_kind_of(OpenStruct) } - it { expect(image.path.to_s).to include('public/ci/build-running.svg') } - it { expect(image.name).to eq('build-running.svg') } - end - - context 'unknown branch name' do - let(:image) { service.execute(project, ref: 'feature') } - - it { expect(image).to be_kind_of(OpenStruct) } - it { expect(image.path.to_s).to include('public/ci/build-unknown.svg') } - it { expect(image.name).to eq('build-unknown.svg') } - end - - context 'commit sha' do - before { build.run! } - let(:image) { service.execute(project, sha: build.sha) } - - it { expect(image).to be_kind_of(OpenStruct) } - it { expect(image.path.to_s).to include('public/ci/build-running.svg') } - it { expect(image.name).to eq('build-running.svg') } - end - - context 'unknown commit sha' do - let(:image) { service.execute(project, sha: '0000000') } - - it { expect(image).to be_kind_of(OpenStruct) } - it { expect(image.path.to_s).to include('public/ci/build-unknown.svg') } - it { expect(image.name).to eq('build-unknown.svg') } - end - end - end -end diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb index ebb11166964..de68fb64726 100644 --- a/spec/services/ci/process_pipeline_service_spec.rb +++ b/spec/services/ci/process_pipeline_service_spec.rb @@ -1,8 +1,16 @@ require 'spec_helper' -describe Ci::ProcessPipelineService, services: true do - let(:pipeline) { create(:ci_empty_pipeline, ref: 'master') } +describe Ci::ProcessPipelineService, :services do let(:user) { create(:user) } + let(:project) { create(:empty_project) } + + let(:pipeline) do + create(:ci_empty_pipeline, ref: 'master', project: project) + end + + before do + project.add_developer(user) + end describe '#execute' do context 'start queuing next builds' do @@ -285,7 +293,7 @@ describe Ci::ProcessPipelineService, services: true do expect(builds.pluck(:name)) .to contain_exactly('build:1', 'build:2', 'test:1', 'test:2') - Ci::Build.retry(pipeline.builds.find_by(name: 'test:2')).success + Ci::Build.retry(pipeline.builds.find_by(name: 'test:2'), user).success expect(builds.pluck(:name)).to contain_exactly( 'build:1', 'build:2', 'test:1', 'test:2', 'test:2', 'deploy:1', 'deploy:2') @@ -333,7 +341,7 @@ describe Ci::ProcessPipelineService, services: true do expect(builds.pending.count).to eq(1) expect(all_builds.count).to eq(4) - # When pending build succeeds in stage test, we enqueue deploy stage. + # When pending merge_when_pipeline_succeeds in stage test, we enqueue deploy stage. # succeed_pending process_pipeline @@ -369,9 +377,7 @@ describe Ci::ProcessPipelineService, services: true do builds.pending.update_all(status: 'success') end - def manual_actions - pipeline.manual_actions - end + delegate :manual_actions, to: :pipeline def create_build(name, stage_idx, when_value = nil) create(:ci_build, diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_build_service_spec.rb index d9f774a1095..cd7dd53025c 100644 --- a/spec/services/ci/register_build_service_spec.rb +++ b/spec/services/ci/register_build_service_spec.rb @@ -170,6 +170,51 @@ module Ci end end + context 'when first build is stalled' do + before do + pending_build.lock_version = 10 + end + + subject { described_class.new(specific_runner).execute } + + context 'with multiple builds are in queue' do + let!(:other_build) { create :ci_build, pipeline: pipeline } + + before do + allow_any_instance_of(Ci::RegisterBuildService).to receive(:builds_for_specific_runner) + .and_return([pending_build, other_build]) + end + + it "receives second build from the queue" do + expect(subject).to be_valid + expect(subject.build).to eq(other_build) + end + end + + context 'when single build is in queue' do + before do + allow_any_instance_of(Ci::RegisterBuildService).to receive(:builds_for_specific_runner) + .and_return([pending_build]) + end + + it "does not receive any valid result" do + expect(subject).not_to be_valid + end + end + + context 'when there is no build in queue' do + before do + allow_any_instance_of(Ci::RegisterBuildService).to receive(:builds_for_specific_runner) + .and_return([]) + end + + it "does not receive builds but result is valid" do + expect(subject).to be_valid + expect(subject.build).to be_nil + end + end + end + def execute(runner) described_class.new(runner).execute.build end diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb new file mode 100644 index 00000000000..65af4e13118 --- /dev/null +++ b/spec/services/ci/retry_build_service_spec.rb @@ -0,0 +1,142 @@ +require 'spec_helper' + +describe Ci::RetryBuildService, :services do + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:build) { create(:ci_build, pipeline: pipeline) } + + let(:service) do + described_class.new(project, user) + end + + CLONE_ACCESSORS = described_class::CLONE_ACCESSORS + + REJECT_ACCESSORS = + %i[id status user token coverage trace runner artifacts_expire_at + artifacts_file artifacts_metadata artifacts_size created_at + updated_at started_at finished_at queued_at erased_by + erased_at].freeze + + IGNORE_ACCESSORS = + %i[type lock_version target_url gl_project_id deploy job_id base_tags + commit_id deployments erased_by_id last_deployment project_id + runner_id tag_taggings taggings tags trigger_request_id + user_id].freeze + + shared_examples 'build duplication' do + let(:build) do + create(:ci_build, :failed, :artifacts_expired, :erased, + :queued, :coverage, :tags, :allowed_to_fail, :on_tag, + :teardown_environment, :triggered, :trace, + description: 'some build', pipeline: pipeline) + end + + describe 'clone accessors' do + CLONE_ACCESSORS.each do |attribute| + it "clones #{attribute} build attribute" do + expect(new_build.send(attribute)).to be_present + expect(new_build.send(attribute)).to eq build.send(attribute) + end + end + end + + describe 'reject acessors' do + REJECT_ACCESSORS.each do |attribute| + it "does not clone #{attribute} build attribute" do + expect(new_build.send(attribute)).not_to eq build.send(attribute) + end + end + end + + it 'has correct number of known attributes' do + known_accessors = CLONE_ACCESSORS + REJECT_ACCESSORS + IGNORE_ACCESSORS + + # :tag_list is a special case, this accessor does not exist + # in reflected associations, comes from `act_as_taggable` and + # we use it to copy tags, instead of reusing tags. + # + current_accessors = + Ci::Build.attribute_names.map(&:to_sym) + + Ci::Build.reflect_on_all_associations.map(&:name) + + [:tag_list] + + current_accessors.uniq! + + expect(known_accessors).to contain_exactly(*current_accessors) + end + end + + describe '#execute' do + let(:new_build) { service.execute(build) } + + context 'when user has ability to execute build' do + before do + project.add_developer(user) + end + + it_behaves_like 'build duplication' + + it 'creates a new build that represents the old one' do + expect(new_build.name).to eq build.name + end + + it 'enqueues the new build' do + expect(new_build).to be_pending + end + + it 'resolves todos for old build that failed' do + expect(MergeRequests::AddTodoWhenBuildFailsService) + .to receive_message_chain(:new, :close) + + service.execute(build) + end + + context 'when there are subsequent builds that are skipped' do + let!(:subsequent_build) do + create(:ci_build, :skipped, stage_idx: 1, pipeline: pipeline) + end + + it 'resumes pipeline processing in subsequent stages' do + service.execute(build) + + expect(subsequent_build.reload).to be_created + end + end + end + + context 'when user does not have ability to execute build' do + it 'raises an error' do + expect { service.execute(build) } + .to raise_error Gitlab::Access::AccessDeniedError + end + end + end + + describe '#reprocess' do + let(:new_build) { service.reprocess(build) } + + context 'when user has ability to execute build' do + before do + project.add_developer(user) + end + + it_behaves_like 'build duplication' + + it 'creates a new build that represents the old one' do + expect(new_build.name).to eq build.name + end + + it 'does not enqueue the new build' do + expect(new_build).to be_created + end + end + + context 'when user does not have ability to execute build' do + it 'raises an error' do + expect { service.reprocess(build) } + .to raise_error Gitlab::Access::AccessDeniedError + end + end + end +end diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb new file mode 100644 index 00000000000..8b1ed6470e4 --- /dev/null +++ b/spec/services/ci/retry_pipeline_service_spec.rb @@ -0,0 +1,196 @@ +require 'spec_helper' + +describe Ci::RetryPipelineService, '#execute', :services do + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:service) { described_class.new(project, user) } + + context 'when user has ability to modify pipeline' do + let(:user) { create(:admin) } + + context 'when there are failed builds in the last stage' do + before do + create_build('rspec 1', :success, 0) + create_build('rspec 2', :failed, 1) + create_build('rspec 3', :canceled, 1) + end + + it 'enqueues all builds in the last stage' do + service.execute(pipeline) + + expect(build('rspec 2')).to be_pending + expect(build('rspec 3')).to be_pending + expect(pipeline.reload).to be_running + end + end + + context 'when there are failed or canceled builds in the first stage' do + before do + create_build('rspec 1', :failed, 0) + create_build('rspec 2', :canceled, 0) + create_build('rspec 3', :canceled, 1) + create_build('spinach 1', :canceled, 2) + end + + it 'retries builds failed builds and marks subsequent for processing' do + service.execute(pipeline) + + expect(build('rspec 1')).to be_pending + expect(build('rspec 2')).to be_pending + expect(build('rspec 3')).to be_created + expect(build('spinach 1')).to be_created + expect(pipeline.reload).to be_running + end + end + + context 'when there is failed build present which was run on failure' do + before do + create_build('rspec 1', :failed, 0) + create_build('rspec 2', :canceled, 0) + create_build('rspec 3', :canceled, 1) + create_build('report 1', :failed, 2) + end + + it 'retries builds only in the first stage' do + service.execute(pipeline) + + expect(build('rspec 1')).to be_pending + expect(build('rspec 2')).to be_pending + expect(build('rspec 3')).to be_created + expect(build('report 1')).to be_created + expect(pipeline.reload).to be_running + end + + it 'creates a new job for report job in this case' do + service.execute(pipeline) + + expect(statuses.where(name: 'report 1').first).to be_retried + end + end + + context 'when the last stage was skipepd' do + before do + create_build('build 1', :success, 0) + create_build('test 2', :failed, 1) + create_build('report 3', :skipped, 2) + create_build('report 4', :skipped, 2) + end + + it 'retries builds only in the first stage' do + service.execute(pipeline) + + expect(build('build 1')).to be_success + expect(build('test 2')).to be_pending + expect(build('report 3')).to be_created + expect(build('report 4')).to be_created + expect(pipeline.reload).to be_running + end + end + + context 'when pipeline contains manual actions' do + context 'when there is a canceled manual action in first stage' do + before do + create_build('rspec 1', :failed, 0) + create_build('staging', :canceled, 0, :manual) + create_build('rspec 2', :canceled, 1) + end + + it 'retries builds failed builds and marks subsequent for processing' do + service.execute(pipeline) + + expect(build('rspec 1')).to be_pending + expect(build('staging')).to be_skipped + expect(build('rspec 2')).to be_created + expect(pipeline.reload).to be_running + end + end + + context 'when there is a skipped manual action in last stage' do + before do + create_build('rspec 1', :canceled, 0) + create_build('rspec 2', :skipped, 0, :manual) + create_build('staging', :skipped, 1, :manual) + end + + it 'retries canceled job and reprocesses manual actions' do + service.execute(pipeline) + + expect(build('rspec 1')).to be_pending + expect(build('rspec 2')).to be_skipped + expect(build('staging')).to be_created + expect(pipeline.reload).to be_running + end + end + + context 'when there is a created manual action in the last stage' do + before do + create_build('rspec 1', :canceled, 0) + create_build('staging', :created, 1, :manual) + end + + it 'retries canceled job and does not update the manual action' do + service.execute(pipeline) + + expect(build('rspec 1')).to be_pending + expect(build('staging')).to be_created + expect(pipeline.reload).to be_running + end + end + + context 'when there is a created manual action in the first stage' do + before do + create_build('rspec 1', :canceled, 0) + create_build('staging', :created, 0, :manual) + end + + it 'retries canceled job and skipps the manual action' do + service.execute(pipeline) + + expect(build('rspec 1')).to be_pending + expect(build('staging')).to be_skipped + expect(pipeline.reload).to be_running + end + end + end + + it 'closes all todos about failed jobs for pipeline' do + expect(MergeRequests::AddTodoWhenBuildFailsService) + .to receive_message_chain(:new, :close_all) + + service.execute(pipeline) + end + + it 'reprocesses the pipeline' do + expect(pipeline).to receive(:process!) + + service.execute(pipeline) + end + end + + context 'when user is not allowed to retry pipeline' do + it 'raises an error' do + expect { service.execute(pipeline) } + .to raise_error Gitlab::Access::AccessDeniedError + end + end + + def statuses + pipeline.reload.statuses + end + + def build(name) + statuses.latest.find_by(name: name) + end + + def create_build(name, status, stage_num, on = 'on_success') + create(:ci_build, name: name, + status: status, + stage: "stage_#{stage_num}", + stage_idx: stage_num, + when: on, + pipeline: pipeline) do |build| + pipeline.update_status + end + end +end diff --git a/spec/services/ci/update_runner_service_spec.rb b/spec/services/ci/update_runner_service_spec.rb new file mode 100644 index 00000000000..e429fcfc72f --- /dev/null +++ b/spec/services/ci/update_runner_service_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe Ci::UpdateRunnerService, :services do + let(:runner) { create(:ci_runner) } + + describe '#update' do + before do + allow(runner).to receive(:tick_runner_queue) + end + + context 'with description params' do + let(:params) { { description: 'new runner' } } + + it 'updates the runner and ticking the queue' do + expect(update).to be_truthy + + runner.reload + + expect(runner).to have_received(:tick_runner_queue) + expect(runner.description).to eq('new runner') + end + end + + context 'when params are not valid' do + let(:params) { { run_untagged: false } } + + it 'does not update and give false because it is not valid' do + expect(update).to be_falsey + + runner.reload + + expect(runner).not_to have_received(:tick_runner_queue) + expect(runner.run_untagged).to be_truthy + end + end + + def update + described_class.new(runner).update(params) + end + end +end diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb index cf0a18aacec..18b964e2453 100644 --- a/spec/services/create_deployment_service_spec.rb +++ b/spec/services/create_deployment_service_spec.rb @@ -9,7 +9,8 @@ describe CreateDeploymentService, services: true do describe '#execute' do let(:options) { nil } let(:params) do - { environment: 'production', + { + environment: 'production', ref: 'master', tag: false, sha: '97de212e80737a608d939f648d959671fb0a0142', @@ -83,10 +84,11 @@ describe CreateDeploymentService, services: true do context 'for environment with invalid name' do let(:params) do - { environment: 'name,with,commas', + { + environment: 'name,with,commas', ref: 'master', tag: false, - sha: '97de212e80737a608d939f648d959671fb0a0142', + sha: '97de212e80737a608d939f648d959671fb0a0142' } end @@ -101,7 +103,8 @@ describe CreateDeploymentService, services: true do context 'when variables are used' do let(:params) do - { environment: 'review-apps/$CI_BUILD_REF_NAME', + { + environment: 'review-apps/$CI_BUILD_REF_NAME', ref: 'master', tag: false, sha: '97de212e80737a608d939f648d959671fb0a0142', @@ -234,7 +237,11 @@ describe CreateDeploymentService, services: true do context 'when build is retried' do it_behaves_like 'does create environment and deployment' do - let(:deployable) { Ci::Build.retry(build) } + before do + project.add_developer(user) + end + + let(:deployable) { Ci::Build.retry(build, user) } subject { deployable.success } end diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb index f86189b68e9..98c560ffb26 100644 --- a/spec/services/groups/destroy_service_spec.rb +++ b/spec/services/groups/destroy_service_spec.rb @@ -5,18 +5,24 @@ describe Groups::DestroyService, services: true do let!(:user) { create(:user) } let!(:group) { create(:group) } + let!(:nested_group) { create(:group, parent: group) } let!(:project) { create(:project, namespace: group) } let!(:gitlab_shell) { Gitlab::Shell.new } let!(:remove_path) { group.path + "+#{group.id}+deleted" } + before do + group.add_user(user, Gitlab::Access::OWNER) + end + shared_examples 'group destruction' do |async| context 'database records' do before do destroy_group(group, user, async) end - it { expect(Group.all).not_to include(group) } - it { expect(Project.all).not_to include(project) } + it { expect(Group.unscoped.all).not_to include(group) } + it { expect(Group.unscoped.all).not_to include(nested_group) } + it { expect(Project.unscoped.all).not_to include(project) } end context 'file system' do @@ -32,7 +38,7 @@ describe Groups::DestroyService, services: true do context 'Sidekiq fake' do before do - # Dont run sidekiq to check if renamed repository exists + # Don't run sidekiq to check if renamed repository exists Sidekiq::Testing.fake! { destroy_group(group, user, async) } end @@ -95,4 +101,13 @@ describe Groups::DestroyService, services: true do describe 'synchronous delete' do it_behaves_like 'group destruction', false end + + context 'projects in pending_delete' do + before do + project.pending_delete = true + project.save + end + + it_behaves_like 'group destruction', false + end end diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb index 4cfba35c830..09807e5d35b 100644 --- a/spec/services/issues/build_service_spec.rb +++ b/spec/services/issues/build_service_spec.rb @@ -120,11 +120,20 @@ describe Issues::BuildService, services: true do end describe '#execute' do + let(:milestone) { create(:milestone, project: project) } + it 'builds a new issues with given params' do - issue = described_class.new(project, user, title: 'Issue #1', description: 'Issue description').execute + issue = described_class.new( + project, + user, + title: 'Issue #1', + description: 'Issue description', + milestone_id: milestone.id, + ).execute expect(issue.title).to eq('Issue #1') expect(issue.description).to eq('Issue description') + expect(issue.milestone).to eq(milestone) end end end diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index 30578ee4c7d..6045d00ff09 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -46,6 +46,7 @@ describe Issues::CreateService, services: true do expect(issue).to be_persisted expect(issue.title).to eq('Awesome issue') + expect(issue.description).to eq('please fix') expect(issue.assignee).to be_nil expect(issue.labels).to be_empty expect(issue.milestone).to be_nil @@ -229,16 +230,6 @@ describe Issues::CreateService, services: true do expect { issue }.not_to change{SpamLog.last.recaptcha_verified} end end - - context 'when spam log title does not match the issue title' do - before do - opts[:title] = 'Another issue' - end - - it 'does not mark spam_log as recaptcha_verified' do - expect { issue }.not_to change{SpamLog.last.recaptcha_verified} - end - end end context 'when recaptcha was not verified' do diff --git a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb index bb7830c7eea..d80fb8a1af1 100644 --- a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb +++ b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb @@ -17,7 +17,7 @@ describe MergeRequests::AddTodoWhenBuildFailsService do described_class.new(project, user, commit_message: 'Awesome message') end - let(:todo_service) { TodoService.new } + let(:todo_service) { spy('todo service') } let(:merge_request) do create(:merge_request, merge_user: user, @@ -107,4 +107,27 @@ describe MergeRequests::AddTodoWhenBuildFailsService do end end end + + describe '#close_all' do + context 'when using pipeline that belongs to merge request' do + it 'resolves todos about failed builds for pipeline' do + service.close_all(pipeline) + + expect(todo_service) + .to have_received(:merge_request_build_retried) + .with(merge_request) + end + end + + context 'when pipeline is not related to merge request' do + let(:pipeline) { create(:ci_empty_pipeline) } + + it 'does not resolve any todos about failed builds' do + service.close_all(pipeline) + + expect(todo_service) + .not_to have_received(:merge_request_build_retried) + end + end + end end diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb index dc945ca4868..0768f644036 100644 --- a/spec/services/merge_requests/build_service_spec.rb +++ b/spec/services/merge_requests/build_service_spec.rb @@ -44,15 +44,14 @@ describe MergeRequests::BuildService, services: true do end end - context 'missing target branch' do - let(:target_branch) { '' } + context 'when target branch is missing' do + let(:target_branch) { nil } + let(:commits) { Commit.decorate([commit_1], project) } - it 'forbids the merge request from being created' do + it 'creates compare object with target branch as default branch' do expect(merge_request.can_be_created).to eq(false) - end - - it 'adds an error message to the merge request' do - expect(merge_request.errors).to contain_exactly('You must select source and target branch') + expect(merge_request.compare).to be_present + expect(merge_request.target_branch).to eq(project.default_branch) end end diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index 5a89acc96a4..d96f819e66a 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -149,35 +149,46 @@ describe MergeRequests::MergeService, services: true do context "error handling" do let(:service) { MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') } - it 'saves error if there is an exception' do - allow(service).to receive(:repository).and_raise("error message") + before do + allow(Rails.logger).to receive(:error) + end + it 'logs and saves error if there is an exception' do + error_message = 'error message' + + allow(service).to receive(:repository).and_raise("error message") allow(service).to receive(:execute_hooks) service.execute(merge_request) - expect(merge_request.merge_error).to eq("Something went wrong during merge: error message") + expect(merge_request.merge_error).to include(error_message) + expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message)) end - it 'saves error if there is an PreReceiveError exception' do - allow(service).to receive(:repository).and_raise(GitHooksService::PreReceiveError, "error") + it 'logs and saves error if there is an PreReceiveError exception' do + error_message = 'error message' + allow(service).to receive(:repository).and_raise(GitHooksService::PreReceiveError, error_message) allow(service).to receive(:execute_hooks) service.execute(merge_request) - expect(merge_request.merge_error).to eq("error") + expect(merge_request.merge_error).to include(error_message) + expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message)) end - it 'aborts if there is a merge conflict' do + it 'logs and saves error if there is a merge conflict' do + error_message = 'Conflicts detected during merge' + allow_any_instance_of(Repository).to receive(:merge).and_return(false) allow(service).to receive(:execute_hooks) service.execute(merge_request) - expect(merge_request.open?).to be_truthy + expect(merge_request).to be_open expect(merge_request.merge_commit_sha).to be_nil - expect(merge_request.merge_error).to eq("Conflicts detected during merge") + expect(merge_request.merge_error).to include(error_message) + expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message)) end end end diff --git a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb index f92978a33a3..c2f205c389d 100644 --- a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb +++ b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb @@ -5,7 +5,7 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do let(:project) { create(:project) } let(:mr_merge_if_green_enabled) do - create(:merge_request, merge_when_build_succeeds: true, merge_user: user, + create(:merge_request, merge_when_pipeline_succeeds: true, merge_user: user, source_branch: "master", target_branch: 'feature', source_project: project, target_project: project, state: "opened") end @@ -36,7 +36,7 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do it 'sets the params, merge_user, and flag' do expect(merge_request).to be_valid - expect(merge_request.merge_when_build_succeeds).to be_truthy + expect(merge_request.merge_when_pipeline_succeeds).to be_truthy expect(merge_request.merge_params).to eq commit_message: 'Awesome message' expect(merge_request.merge_user).to be user end @@ -62,7 +62,7 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do end it 'updates the merge params' do - expect(SystemNoteService).not_to receive(:merge_when_build_succeeds) + expect(SystemNoteService).not_to receive(:merge_when_pipeline_succeeds) service.execute(mr_merge_if_green_enabled) expect(mr_merge_if_green_enabled.merge_params).to have_key(:new_key) @@ -82,7 +82,7 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do sha: merge_request_head, status: 'success') end - it "merges all merge requests with merge when build succeeds enabled" do + it "merges all merge requests with merge when the pipeline succeeds enabled" do expect(MergeWorker).to receive(:perform_async) service.trigger(triggering_pipeline) end @@ -111,6 +111,31 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do service.trigger(unrelated_pipeline) end end + + context 'when the merge request is not mergeable' do + let(:mr_conflict) do + create(:merge_request, merge_when_pipeline_succeeds: true, merge_user: user, + source_branch: 'master', target_branch: 'feature-conflict', + source_project: project, target_project: project) + end + + let(:conflict_pipeline) do + create(:ci_pipeline, project: project, ref: mr_conflict.source_branch, + sha: mr_conflict.diff_head_sha, status: 'success') + end + + it 'does not merge the merge request' do + expect(MergeWorker).not_to receive(:perform_async) + + service.trigger(conflict_pipeline) + end + + it 'creates todos for unmergeability' do + expect_any_instance_of(TodoService).to receive(:merge_request_became_unmergeable).with(mr_conflict) + + service.trigger(conflict_pipeline) + end + end end describe "#cancel" do @@ -118,8 +143,8 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do service.cancel(mr_merge_if_green_enabled) end - it "resets all the merge_when_build_succeeds params" do - expect(mr_merge_if_green_enabled.merge_when_build_succeeds).to be_falsey + it "resets all the pipeline succeeds params" do + expect(mr_merge_if_green_enabled.merge_when_pipeline_succeeds).to be_falsey expect(mr_merge_if_green_enabled.merge_params).to eq({}) expect(mr_merge_if_green_enabled.merge_user).to be nil end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 2cc21acab7b..ff367f54d2a 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -18,7 +18,7 @@ describe MergeRequests::RefreshService, services: true do source_branch: 'master', target_branch: 'feature', target_project: @project, - merge_when_build_succeeds: true, + merge_when_pipeline_succeeds: true, merge_user: @user) @fork_merge_request = create(:merge_request, @@ -62,7 +62,7 @@ describe MergeRequests::RefreshService, services: true do it { expect(@merge_request.notes).not_to be_empty } it { expect(@merge_request).to be_open } - it { expect(@merge_request.merge_when_build_succeeds).to be_falsey } + it { expect(@merge_request.merge_when_pipeline_succeeds).to be_falsey } it { expect(@merge_request.diff_head_sha).to eq(@newrev) } it { expect(@fork_merge_request).to be_open } it { expect(@fork_merge_request.notes).to be_empty } @@ -287,41 +287,64 @@ describe MergeRequests::RefreshService, services: true do it 'references the commit that caused the Work in Progress status' do refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') - allow(refresh_service).to receive(:find_new_commits) refresh_service.instance_variable_set("@commits", [ - instance_double( - Commit, + double( id: 'aaaaaaa', + sha: '38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e', short_id: 'aaaaaaa', title: 'Fix issue', work_in_progress?: false ), - instance_double( - Commit, + double( id: 'bbbbbbb', + sha: '498214de67004b1da3d820901307bed2a68a8ef6', short_id: 'bbbbbbb', title: 'fixup! Fix issue', work_in_progress?: true, to_reference: 'bbbbbbb' ), - instance_double( - Commit, + double( id: 'ccccccc', + sha: '1b12f15a11fc6e62177bef08f47bc7b5ce50b141', short_id: 'ccccccc', title: 'fixup! Fix issue', work_in_progress?: true, to_reference: 'ccccccc' ), ]) - refresh_service.execute(@oldrev, @newrev, 'refs/heads/wip') reload_mrs - expect(@merge_request.notes.last.note).to eq( "marked as a **Work In Progress** from bbbbbbb" ) end + + it 'does not mark as WIP based on commits that do not belong to an MR' do + allow(refresh_service).to receive(:find_new_commits) + refresh_service.instance_variable_set("@commits", [ + double( + id: 'aaaaaaa', + sha: 'aaaaaaa', + short_id: 'aaaaaaa', + title: 'Fix issue', + work_in_progress?: false + ), + double( + id: 'bbbbbbb', + sha: 'bbbbbbbb', + short_id: 'bbbbbbb', + title: 'fixup! Fix issue', + work_in_progress?: true, + to_reference: 'bbbbbbb' + ) + ]) + + refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') + reload_mrs + + expect(@merge_request.work_in_progress?).to be_falsey + end end def reload_mrs diff --git a/spec/services/merge_requests/resolve_service_spec.rb b/spec/services/merge_requests/resolve_service_spec.rb index a0e51681725..d33535d22af 100644 --- a/spec/services/merge_requests/resolve_service_spec.rb +++ b/spec/services/merge_requests/resolve_service_spec.rb @@ -59,20 +59,19 @@ describe MergeRequests::ResolveService do it 'creates a commit with the correct parents' do expect(merge_request.source_branch_head.parents.map(&:id)). - to eq(['1450cd639e0bc6721eb02800169e464f212cde06', - '824be604a34828eb682305f0d963056cfac87b2d']) + to eq(%w(1450cd639e0bc6721eb02800169e464f212cde06 + 824be604a34828eb682305f0d963056cfac87b2d)) 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( + project.repository.create_file( user, 'new-file-in-target', '', message: 'Add new file in target', - branch_name: 'conflict-start', - update: false) + branch_name: 'conflict-start') end before do @@ -125,8 +124,8 @@ describe MergeRequests::ResolveService do it 'creates a commit with the correct parents' do expect(merge_request.source_branch_head.parents.map(&:id)). - to eq(['1450cd639e0bc6721eb02800169e464f212cde06', - '824be604a34828eb682305f0d963056cfac87b2d']) + to eq(%w(1450cd639e0bc6721eb02800169e464f212cde06 + 824be604a34828eb682305f0d963056cfac87b2d)) end it 'sets the content to the content given' do diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index 9c92a5080c6..152c6d20daa 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -102,47 +102,19 @@ describe Notes::CreateService, services: true do expect(subject.note).to eq(params[:note]) end end - end - - describe "award emoji" do - before do - project.team << [user, :master] - end - - it "creates an award emoji" do - opts = { - note: ':smile: ', - noteable_type: 'Issue', - noteable_id: issue.id - } - note = described_class.new(project, user, opts).execute - - expect(note).to be_valid - expect(note.name).to eq('smile') - end - it "creates regular note if emoji name is invalid" do - opts = { - note: ':smile: moretext:', - noteable_type: 'Issue', - noteable_id: issue.id - } - note = described_class.new(project, user, opts).execute - - expect(note).to be_valid - expect(note.note).to eq(opts[:note]) - end - - it "normalizes the emoji name" do - opts = { - note: ':+1:', - noteable_type: 'Issue', - noteable_id: issue.id - } - - expect_any_instance_of(TodoService).to receive(:new_award_emoji).with(issue, user) + describe 'note with emoji only' do + it 'creates regular note' do + opts = { + note: ':smile: ', + noteable_type: 'Issue', + noteable_id: issue.id + } + note = described_class.new(project, user, opts).execute - described_class.new(project, user, opts).execute + expect(note).to be_valid + expect(note.note).to eq(':smile:') + end end end end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 7cf2cd9968f..ebbaea4e59a 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -146,6 +146,16 @@ describe NotificationService, services: true do should_not_email(@u_lazy_participant) end + it "emails the note author if they've opted into notifications about their activity" do + add_users_with_subscription(note.project, issue) + note.author.notified_of_own_activity = true + reset_delivered_emails! + + notification.new_note(note) + + should_email(note.author) + end + it 'filters out "mentioned in" notes' do mentioned_note = SystemNoteService.cross_reference(mentioned_issue, issue, issue.author) @@ -476,6 +486,20 @@ describe NotificationService, services: true do should_not_email(issue.assignee) end + it "emails the author if they've opted into notifications about their activity" do + issue.author.notified_of_own_activity = true + + notification.new_issue(issue, issue.author) + + should_email(issue.author) + end + + it "doesn't email the author if they haven't opted into notifications about their activity" do + notification.new_issue(issue, issue.author) + + should_not_email(issue.author) + end + it "emails subscribers of the issue's labels" do user_1 = create(:user) user_2 = create(:user) @@ -665,6 +689,19 @@ describe NotificationService, services: true do should_email(subscriber_to_label_2) end + it "emails the current user if they've opted into notifications about their activity" do + subscriber_to_label_2.notified_of_own_activity = true + notification.relabeled_issue(issue, [group_label_2, label_2], subscriber_to_label_2) + + should_email(subscriber_to_label_2) + end + + it "doesn't email the current user if they haven't opted into notifications about their activity" do + notification.relabeled_issue(issue, [group_label_2, label_2], subscriber_to_label_2) + + should_not_email(subscriber_to_label_2) + end + it "doesn't send email to anyone but subscribers of the given labels" do notification.relabeled_issue(issue, [group_label_2, label_2], @u_disabled) @@ -818,6 +855,20 @@ describe NotificationService, services: true do should_not_email(@u_lazy_participant) end + it "emails the author if they've opted into notifications about their activity" do + merge_request.author.notified_of_own_activity = true + + notification.new_merge_request(merge_request, merge_request.author) + + should_email(merge_request.author) + end + + it "doesn't email the author if they haven't opted into notifications about their activity" do + notification.new_merge_request(merge_request, merge_request.author) + + should_not_email(merge_request.author) + end + it "emails subscribers of the merge request's labels" do user_1 = create(:user) user_2 = create(:user) @@ -999,20 +1050,28 @@ describe NotificationService, services: true do should_not_email(@u_lazy_participant) end - it "notifies the merger when merge_when_build_succeeds is true" do - merge_request.merge_when_build_succeeds = true + it "notifies the merger when the pipeline succeeds is true" do + merge_request.merge_when_pipeline_succeeds = true notification.merge_mr(merge_request, @u_watcher) should_email(@u_watcher) end - it "does not notify the merger when merge_when_build_succeeds is false" do - merge_request.merge_when_build_succeeds = false + it "does not notify the merger when the pipeline succeeds is false" do + merge_request.merge_when_pipeline_succeeds = false notification.merge_mr(merge_request, @u_watcher) should_not_email(@u_watcher) end + it "notifies the merger when the pipeline succeeds is false but they've opted into notifications about their activity" do + merge_request.merge_when_pipeline_succeeds = false + @u_watcher.notified_of_own_activity = true + notification.merge_mr(merge_request, @u_watcher) + + should_email(@u_watcher) + end + it_behaves_like 'participating notifications' do let(:participant) { create(:user, username: 'user-participant') } let(:issuable) { merge_request } diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index af515ad2e0e..62f21049b0b 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -50,7 +50,7 @@ describe Projects::CreateService, '#execute', services: true do context 'error handling' do it 'handles invalid options' do - opts.merge!({ default_branch: 'master' } ) + opts[:default_branch] = 'master' expect(create_project(user, opts)).to eq(nil) end end @@ -67,7 +67,7 @@ describe Projects::CreateService, '#execute', services: true do context 'wiki_enabled false does not create wiki repository directory' do it do - opts.merge!(wiki_enabled: false) + opts[:wiki_enabled] = false project = create_project(user, opts) path = ProjectWiki.new(project, user).send(:path_to_repo) diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index 3faa88c00a1..74bfba44dfd 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -50,6 +50,25 @@ describe Projects::DestroyService, services: true do it { expect(Dir.exist?(remove_path)).to be_truthy } end + context 'when flushing caches fail' do + before do + new_user = create(:user) + project.team.add_user(new_user, Gitlab::Access::DEVELOPER) + allow_any_instance_of(Projects::DestroyService).to receive(:flush_caches).and_raise(Redis::CannotConnectError) + end + + it 'keeps project team intact upon an error' do + Sidekiq::Testing.inline! do + begin + destroy_project(project, user, {}) + rescue Redis::CannotConnectError + end + end + + expect(project.team.members.count).to eq 1 + end + end + context 'with async_execute' do let(:async) { true } diff --git a/spec/services/protected_branches/create_service_spec.rb b/spec/services/protected_branches/create_service_spec.rb index 7d4eff3b6ef..6ea8f309981 100644 --- a/spec/services/protected_branches/create_service_spec.rb +++ b/spec/services/protected_branches/create_service_spec.rb @@ -6,8 +6,8 @@ describe ProtectedBranches::CreateService, services: true do let(:params) do { name: 'master', - merge_access_levels_attributes: [ { access_level: Gitlab::Access::MASTER } ], - push_access_levels_attributes: [ { access_level: Gitlab::Access::MASTER } ] + merge_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }], + push_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }] } end diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb index 0b0925983eb..52e8678cb9d 100644 --- a/spec/services/slash_commands/interpret_service_spec.rb +++ b/spec/services/slash_commands/interpret_service_spec.rb @@ -267,6 +267,14 @@ describe SlashCommands::InterpretService, services: true do end end + shared_examples 'award command' do + it 'toggle award 100 emoji if content containts /award :100:' do + _, updates = service.execute(content, issuable) + + expect(updates).to eq(emoji_award: "100") + end + end + it_behaves_like 'reopen command' do let(:content) { '/reopen' } let(:issuable) { issue } @@ -654,6 +662,37 @@ describe SlashCommands::InterpretService, services: true do end end + context '/award command' do + it_behaves_like 'award command' do + let(:content) { '/award :100:' } + let(:issuable) { issue } + end + + it_behaves_like 'award command' do + let(:content) { '/award :100:' } + let(:issuable) { merge_request } + end + + context 'ignores command with no argument' do + it_behaves_like 'empty command' do + let(:content) { '/award' } + let(:issuable) { issue } + end + end + + context 'ignores non-existing / invalid emojis' do + it_behaves_like 'empty command' do + let(:content) { '/award noop' } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { '/award :lorem_ipsum:' } + let(:issuable) { issue } + end + end + end + context '/target_branch command' do let(:non_empty_project) { create(:project) } let(:another_merge_request) { create(:merge_request, author: developer, source_project: non_empty_project) } diff --git a/spec/services/spam_service_spec.rb b/spec/services/spam_service_spec.rb index 271c17dd8c0..4ce3b95aa87 100644 --- a/spec/services/spam_service_spec.rb +++ b/spec/services/spam_service_spec.rb @@ -1,46 +1,61 @@ require 'spec_helper' describe SpamService, services: true do - describe '#check' do - let(:project) { create(:project, :public) } - let(:issue) { create(:issue, project: project) } - let(:request) { double(:request, env: {}) } + describe '#when_recaptcha_verified' do + def check_spam(issue, request, recaptcha_verified) + described_class.new(issue, request).when_recaptcha_verified(recaptcha_verified) do + 'yielded' + end + end + + it 'yields block when recaptcha was already verified' do + issue = build_stubbed(:issue) - def check_spam(issue, request) - described_class.new(issue, request).check + expect(check_spam(issue, nil, true)).to eql('yielded') end - context 'when indicated as spam by akismet' do - before { allow(AkismetService).to receive(:new).and_return(double(is_spam?: true)) } + context 'when recaptcha was not verified' do + let(:project) { create(:project, :public) } + let(:issue) { create(:issue, project: project) } + let(:request) { double(:request, env: {}) } - it 'returns false when request is missing' do - expect(check_spam(issue, nil)).to be_falsey - end + context 'when indicated as spam by akismet' do + before { allow(AkismetService).to receive(:new).and_return(double(is_spam?: true)) } - it 'returns false when issue is not public' do - issue = create(:issue, project: create(:project, :private)) + it 'doesnt check as spam when request is missing' do + check_spam(issue, nil, false) - expect(check_spam(issue, request)).to be_falsey - end + expect(issue.spam).to be_falsey + end - it 'returns true' do - expect(check_spam(issue, request)).to be_truthy - end + it 'checks as spam' do + check_spam(issue, request, false) - it 'creates a spam log' do - expect { check_spam(issue, request) }.to change { SpamLog.count }.from(0).to(1) - end - end + expect(issue.spam).to be_truthy + end - context 'when not indicated as spam by akismet' do - before { allow(AkismetService).to receive(:new).and_return(double(is_spam?: false)) } + it 'creates a spam log' do + expect { check_spam(issue, request, false) } + .to change { SpamLog.count }.from(0).to(1) + end - it 'returns false' do - expect(check_spam(issue, request)).to be_falsey + it 'doesnt yield block' do + expect(check_spam(issue, request, false)) + .to eql(SpamLog.last) + end end - it 'does not create a spam log' do - expect { check_spam(issue, request) }.not_to change { SpamLog.count } + context 'when not indicated as spam by akismet' do + before { allow(AkismetService).to receive(:new).and_return(double(is_spam?: false)) } + + it 'returns false' do + expect(check_spam(issue, request, false)).to be_falsey + end + + it 'does not create a spam log' do + expect { check_spam(issue, request, false) } + .not_to change { SpamLog.count } + end end end end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 7f027ae02a2..36a17a3bf2e 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -215,13 +215,13 @@ describe SystemNoteService, services: true do end end - describe '.merge_when_build_succeeds' do + describe '.merge_when_pipeline_succeeds' do let(:pipeline) { build(:ci_pipeline_without_jobs )} let(:noteable) do create(:merge_request, source_project: project, target_project: project) end - subject { described_class.merge_when_build_succeeds(noteable, project, author, noteable.diff_head_commit) } + subject { described_class.merge_when_pipeline_succeeds(noteable, project, author, noteable.diff_head_commit) } it_behaves_like 'a system note' @@ -230,12 +230,12 @@ describe SystemNoteService, services: true do end end - describe '.cancel_merge_when_build_succeeds' do + describe '.cancel_merge_when_pipeline_succeeds' do let(:noteable) do create(:merge_request, source_project: project, target_project: project) end - subject { described_class.cancel_merge_when_build_succeeds(noteable, project, author) } + subject { described_class.cancel_merge_when_pipeline_succeeds(noteable, project, author) } it_behaves_like 'a system note' @@ -418,45 +418,6 @@ describe SystemNoteService, services: true do to be_truthy end end - - context 'when noteable is an Issue' do - let(:issue) { create(:issue, project: project) } - - it 'is truthy when issue is closed' do - issue.close - - expect(described_class.cross_reference_disallowed?(issue, project.commit)). - to be_truthy - end - - it 'is falsey when issue is open' do - expect(described_class.cross_reference_disallowed?(issue, project.commit)). - to be_falsy - end - end - - context 'when noteable is a Merge Request' do - let(:merge_request) { create(:merge_request, :simple, source_project: project) } - - it 'is truthy when merge request is closed' do - allow(merge_request).to receive(:closed?).and_return(:true) - - expect(described_class.cross_reference_disallowed?(merge_request, project.commit)). - to be_truthy - end - - it 'is truthy when merge request is merged' do - allow(merge_request).to receive(:closed?).and_return(:true) - - expect(described_class.cross_reference_disallowed?(merge_request, project.commit)). - to be_truthy - end - - it 'is falsey when merge request is open' do - expect(described_class.cross_reference_disallowed?(merge_request, project.commit)). - to be_falsy - end - end end describe '.cross_reference_exists?' do @@ -631,7 +592,7 @@ describe SystemNoteService, services: true do jira_service_settings end - noteable_types = ["merge_requests", "commit"] + noteable_types = %w(merge_requests commit) noteable_types.each do |type| context "when noteable is a #{type}" do diff --git a/spec/services/create_tag_service_spec.rb b/spec/services/tags/create_service_spec.rb index 7dc43c50b0d..5478b8c9ec0 100644 --- a/spec/services/create_tag_service_spec.rb +++ b/spec/services/tags/create_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe CreateTagService, services: true do +describe Tags::CreateService, services: true do let(:project) { create(:project) } let(:repository) { project.repository } let(:user) { create(:user) } diff --git a/spec/services/delete_tag_service_spec.rb b/spec/services/tags/destroy_service_spec.rb index 477551f5036..a388c93379a 100644 --- a/spec/services/delete_tag_service_spec.rb +++ b/spec/services/tags/destroy_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe DeleteTagService, services: true do +describe Tags::DestroyService, services: true do let(:project) { create(:project) } let(:repository) { project.repository } let(:user) { create(:user) } diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 13d584a8975..fb9a8462f84 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -9,7 +9,9 @@ describe TodoService, services: true do let(:admin) { create(:admin) } let(:john_doe) { create(:user) } let(:project) { create(:project) } - let(:mentions) { [author, assignee, john_doe, member, guest, non_member, admin].map(&:to_reference).join(' ') } + let(:mentions) { 'FYI: ' + [author, assignee, john_doe, member, guest, non_member, admin].map(&:to_reference).join(' ') } + let(:directly_addressed) { [author, assignee, john_doe, member, guest, non_member, admin].map(&:to_reference).join(' ') } + let(:directly_addressed_and_mentioned) { member.to_reference + ", what do you think? cc: " + [guest, admin].map(&:to_reference).join(' ') } let(:service) { described_class.new } before do @@ -21,8 +23,10 @@ describe TodoService, services: true do describe 'Issues' do let(:issue) { create(:issue, project: project, assignee: john_doe, author: author, description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") } + let(:addressed_issue) { create(:issue, project: project, assignee: john_doe, author: author, description: "#{directly_addressed}\n- [ ] Task 1\n- [ ] Task 2") } let(:unassigned_issue) { create(:issue, project: project, assignee: nil) } let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee, description: mentions) } + let(:addressed_confident_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee, description: directly_addressed) } describe '#new_issue' do it 'creates a todo if assigned' do @@ -52,6 +56,26 @@ describe TodoService, services: true do should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED) end + it 'creates a directly addressed todo for each valid addressed user' do + service.new_issue(addressed_issue, author) + + should_create_todo(user: member, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: guest, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: author, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: john_doe, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: non_member, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) + end + + it 'creates correct todos for each valid user based on the type of mention' do + issue.update(description: directly_addressed_and_mentioned) + + service.new_issue(issue, author) + + should_create_todo(user: member, target: issue, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: admin, target: issue, action: Todo::MENTIONED) + should_create_todo(user: guest, target: issue, action: Todo::MENTIONED) + end + it 'does not create todo if user can not see the issue when issue is confidential' do service.new_issue(confidential_issue, john_doe) @@ -63,6 +87,17 @@ describe TodoService, services: true do should_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) end + it 'does not create directly addressed todo if user cannot see the issue when issue is confidential' do + service.new_issue(addressed_confident_issue, john_doe) + + should_create_todo(user: assignee, target: addressed_confident_issue, author: john_doe, action: Todo::ASSIGNED) + should_create_todo(user: author, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: member, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: admin, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: guest, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: john_doe, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED) + end + context 'when a private group is mentioned' do let(:group) { create :group, :private } let(:project) { create :project, :private, group: group } @@ -94,12 +129,38 @@ describe TodoService, services: true do should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED) end + it 'creates a todo for each valid user based on the type of mention' do + issue.update(description: directly_addressed_and_mentioned) + + service.update_issue(issue, author) + + should_create_todo(user: member, target: issue, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: guest, target: issue, action: Todo::MENTIONED) + should_create_todo(user: admin, target: issue, action: Todo::MENTIONED) + end + + it 'creates a directly addressed todo for each valid addressed user' do + service.update_issue(addressed_issue, author) + + should_create_todo(user: member, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: guest, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: john_doe, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: author, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: non_member, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) + end + it 'does not create a todo if user was already mentioned' do create(:todo, :mentioned, user: member, project: project, target: issue, author: author) expect { service.update_issue(issue, author) }.not_to change(member.todos, :count) end + it 'does not create a directly addressed todo if user was already mentioned or addressed' do + create(:todo, :directly_addressed, user: member, project: project, target: addressed_issue, author: author) + + expect { service.update_issue(addressed_issue, author) }.not_to change(member.todos, :count) + end + it 'does not create todo if user can not see the issue when issue is confidential' do service.update_issue(confidential_issue, john_doe) @@ -111,6 +172,17 @@ describe TodoService, services: true do should_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) end + it 'does not create a directly addressed todo if user can not see the issue when issue is confidential' do + service.update_issue(addressed_confident_issue, john_doe) + + should_create_todo(user: author, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: assignee, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: member, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: admin, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: guest, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: john_doe, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED) + end + context 'issues with a task list' do it 'does not create todo when tasks are marked as completed' do issue.update(description: "- [x] Task 1\n- [X] Task 2 #{mentions}") @@ -125,6 +197,19 @@ describe TodoService, services: true do should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED) end + it 'does not create directly addressed todo when tasks are marked as completed' do + addressed_issue.update(description: "#{directly_addressed}\n- [x] Task 1\n- [x] Task 2\n") + + service.update_issue(addressed_issue, author) + + should_not_create_todo(user: admin, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: assignee, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: author, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: john_doe, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: member, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: non_member, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED) + end + it 'does not raise an error when description not change' do issue.update(title: 'Sample') @@ -202,39 +287,51 @@ describe TodoService, services: true do end end - shared_examples 'marking todos as done' do |meth| - let!(:first_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) } - let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) } + shared_examples 'updating todos state' do |meth, state, new_state| + let!(:first_todo) { create(:todo, state, user: john_doe, project: project, target: issue, author: author) } + let!(:second_todo) { create(:todo, state, user: john_doe, project: project, target: issue, author: author) } - it 'marks related todos for the user as done' do + it 'updates related todos for the user with the new_state' do service.send(meth, collection, john_doe) - expect(first_todo.reload).to be_done - expect(second_todo.reload).to be_done + expect(first_todo.reload.state?(new_state)).to be true + expect(second_todo.reload.state?(new_state)).to be true end describe 'cached counts' do it 'updates when todos change' do - expect(john_doe.todos_done_count).to eq(0) - expect(john_doe.todos_pending_count).to eq(2) + expect(john_doe.todos.where(state: new_state).count).to eq(0) + expect(john_doe.todos.where(state: state).count).to eq(2) expect(john_doe).to receive(:update_todos_count_cache).and_call_original service.send(meth, collection, john_doe) - expect(john_doe.todos_done_count).to eq(2) - expect(john_doe.todos_pending_count).to eq(0) + expect(john_doe.todos.where(state: new_state).count).to eq(2) + expect(john_doe.todos.where(state: state).count).to eq(0) end end end describe '#mark_todos_as_done' do - it_behaves_like 'marking todos as done', :mark_todos_as_done do + it_behaves_like 'updating todos state', :mark_todos_as_done, :pending, :done do let(:collection) { [first_todo, second_todo] } end end describe '#mark_todos_as_done_by_ids' do - it_behaves_like 'marking todos as done', :mark_todos_as_done_by_ids do + it_behaves_like 'updating todos state', :mark_todos_as_done_by_ids, :pending, :done do + let(:collection) { [first_todo, second_todo].map(&:id) } + end + end + + describe '#mark_todos_as_pending' do + it_behaves_like 'updating todos state', :mark_todos_as_pending, :done, :pending do + let(:collection) { [first_todo, second_todo] } + end + end + + describe '#mark_todos_as_pending_by_ids' do + it_behaves_like 'updating todos state', :mark_todos_as_pending_by_ids, :done, :pending do let(:collection) { [first_todo, second_todo].map(&:id) } end end @@ -244,8 +341,11 @@ describe TodoService, services: true do let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) } let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) } let(:note) { create(:note, project: project, noteable: issue, author: john_doe, note: mentions) } + let(:addressed_note) { create(:note, project: project, noteable: issue, author: john_doe, note: directly_addressed) } let(:note_on_commit) { create(:note_on_commit, project: project, author: john_doe, note: mentions) } + let(:addressed_note_on_commit) { create(:note_on_commit, project: project, author: john_doe, note: directly_addressed) } let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project, note: mentions) } + let(:addressed_note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project, note: directly_addressed) } let(:note_on_project_snippet) { create(:note_on_project_snippet, project: project, author: john_doe, note: mentions) } let(:system_note) { create(:system_note, project: project, noteable: issue) } @@ -276,6 +376,26 @@ describe TodoService, services: true do should_not_create_todo(user: non_member, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) end + it 'creates a todo for each valid user based on the type of mention' do + note.update(note: directly_addressed_and_mentioned) + + service.new_note(note, john_doe) + + should_create_todo(user: member, target: issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: note) + should_create_todo(user: admin, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) + should_create_todo(user: guest, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) + end + + it 'creates a directly addressed todo for each valid addressed user' do + service.new_note(addressed_note, john_doe) + + should_create_todo(user: member, target: issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note) + should_create_todo(user: guest, target: issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note) + should_create_todo(user: author, target: issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note) + should_create_todo(user: john_doe, target: issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note) + should_not_create_todo(user: non_member, target: issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note) + end + it 'does not create todo if user can not see the issue when leaving a note on a confidential issue' do service.new_note(note_on_confidential_issue, john_doe) @@ -287,6 +407,17 @@ describe TodoService, services: true do should_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue) end + it 'does not create a directly addressed todo if user can not see the issue when leaving a note on a confidential issue' do + service.new_note(addressed_note_on_confidential_issue, john_doe) + + should_create_todo(user: author, target: confidential_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_confidential_issue) + should_create_todo(user: assignee, target: confidential_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_confidential_issue) + should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_confidential_issue) + should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_confidential_issue) + should_not_create_todo(user: guest, target: confidential_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_confidential_issue) + should_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_confidential_issue) + end + it 'creates a todo for each valid mentioned user when leaving a note on commit' do service.new_note(note_on_commit, john_doe) @@ -296,6 +427,15 @@ describe TodoService, services: true do should_not_create_todo(user: non_member, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) end + it 'creates a directly addressed todo for each valid mentioned user when leaving a note on commit' do + service.new_note(addressed_note_on_commit, john_doe) + + should_create_todo(user: member, target_id: nil, target_type: 'Commit', commit_id: addressed_note_on_commit.commit_id, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_commit) + should_create_todo(user: author, target_id: nil, target_type: 'Commit', commit_id: addressed_note_on_commit.commit_id, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_commit) + should_create_todo(user: john_doe, target_id: nil, target_type: 'Commit', commit_id: addressed_note_on_commit.commit_id, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_commit) + should_not_create_todo(user: non_member, target_id: nil, target_type: 'Commit', commit_id: addressed_note_on_commit.commit_id, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_commit) + end + it 'does not create todo when leaving a note on snippet' do should_not_create_any_todo { service.new_note(note_on_project_snippet, john_doe) } end @@ -324,6 +464,7 @@ describe TodoService, services: true do describe 'Merge Requests' do let(:mr_assigned) { create(:merge_request, source_project: project, author: author, assignee: john_doe, description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") } + let(:addressed_mr_assigned) { create(:merge_request, source_project: project, author: author, assignee: john_doe, description: "#{directly_addressed}\n- [ ] Task 1\n- [ ] Task 2") } let(:mr_unassigned) { create(:merge_request, source_project: project, author: author, assignee: nil) } describe '#new_merge_request' do @@ -350,6 +491,25 @@ describe TodoService, services: true do should_not_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED) should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED) end + + it 'creates a todo for each valid user based on the type of mention' do + mr_assigned.update(description: directly_addressed_and_mentioned) + + service.new_merge_request(mr_assigned, author) + + should_create_todo(user: member, target: mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: admin, target: mr_assigned, action: Todo::MENTIONED) + end + + it 'creates a directly addressed todo for each valid addressed user' do + service.new_merge_request(addressed_mr_assigned, author) + + should_create_todo(user: member, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: guest, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: author, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: john_doe, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: non_member, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + end end describe '#update_merge_request' do @@ -363,12 +523,37 @@ describe TodoService, services: true do should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED) end + it 'creates a todo for each valid user based on the type of mention' do + mr_assigned.update(description: directly_addressed_and_mentioned) + + service.update_merge_request(mr_assigned, author) + + should_create_todo(user: member, target: mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: admin, target: mr_assigned, action: Todo::MENTIONED) + end + + it 'creates a directly addressed todo for each valid addressed user' do + service.update_merge_request(addressed_mr_assigned, author) + + should_create_todo(user: member, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: guest, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: john_doe, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + should_create_todo(user: author, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: non_member, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + end + it 'does not create a todo if user was already mentioned' do create(:todo, :mentioned, user: member, project: project, target: mr_assigned, author: author) expect { service.update_merge_request(mr_assigned, author) }.not_to change(member.todos, :count) end + it 'does not create a directly addressed todo if user was already mentioned or addressed' do + create(:todo, :directly_addressed, user: member, project: project, target: addressed_mr_assigned, author: author) + + expect{ service.update_merge_request(addressed_mr_assigned, author) }.not_to change(member.todos, :count) + end + context 'with a task list' do it 'does not create todo when tasks are marked as completed' do mr_assigned.update(description: "- [x] Task 1\n- [X] Task 2 #{mentions}") @@ -384,6 +569,20 @@ describe TodoService, services: true do should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED) end + it 'does not create directly addressed todo when tasks are marked as completed' do + addressed_mr_assigned.update(description: "#{directly_addressed}\n- [x] Task 1\n- [X] Task 2") + + service.update_merge_request(addressed_mr_assigned, author) + + should_not_create_todo(user: admin, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: assignee, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: author, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: john_doe, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: member, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: non_member, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + should_not_create_todo(user: guest, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + end + it 'does not raise an error when description not change' do mr_assigned.update(title: 'Sample') @@ -436,6 +635,11 @@ describe TodoService, services: true do service.reassigned_merge_request(mr_assigned, author) should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED) end + + it 'does not create a directly addressed todo for guests' do + service.reassigned_merge_request(addressed_mr_assigned, author) + should_not_create_todo(user: guest, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + end end describe '#merge_merge_request' do @@ -452,6 +656,11 @@ describe TodoService, services: true do service.merge_merge_request(mr_assigned, john_doe) should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED) end + + it 'does not create directly addressed todo for guests' do + service.merge_merge_request(addressed_mr_assigned, john_doe) + should_not_create_todo(user: guest, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) + end end describe '#new_award_emoji' do @@ -471,7 +680,7 @@ describe TodoService, services: true do end it 'creates a pending todo for merge_user' do - mr_unassigned.update(merge_when_build_succeeds: true, merge_user: admin) + mr_unassigned.update(merge_when_pipeline_succeeds: true, merge_user: admin) service.merge_request_build_failed(mr_unassigned) should_create_todo(user: admin, author: admin, target: mr_unassigned, action: Todo::BUILD_FAILED) @@ -491,7 +700,7 @@ describe TodoService, services: true do describe '#merge_request_became_unmergeable' do it 'creates a pending todo for a merge_user' do - mr_unassigned.update(merge_when_build_succeeds: true, merge_user: admin) + mr_unassigned.update(merge_when_pipeline_succeeds: true, merge_user: admin) service.merge_request_became_unmergeable(mr_unassigned) should_create_todo(user: admin, author: admin, target: mr_unassigned, action: Todo::UNMERGEABLE) @@ -509,6 +718,7 @@ describe TodoService, services: true do describe '#new_note' do let(:mention) { john_doe.to_reference } let(:diff_note_on_merge_request) { create(:diff_note_on_merge_request, project: project, noteable: mr_unassigned, author: author, note: "Hey #{mention}") } + let(:addressed_diff_note_on_merge_request) { create(:diff_note_on_merge_request, project: project, noteable: mr_unassigned, author: author, note: "#{mention}, hey!") } let(:legacy_diff_note_on_merge_request) { create(:legacy_diff_note_on_merge_request, project: project, noteable: mr_unassigned, author: author, note: "Hey #{mention}") } it 'creates a todo for mentioned user on new diff note' do @@ -517,6 +727,12 @@ describe TodoService, services: true do should_create_todo(user: john_doe, target: mr_unassigned, author: author, action: Todo::MENTIONED, note: diff_note_on_merge_request) end + it 'creates a directly addressed todo for addressed user on new diff note' do + service.new_note(addressed_diff_note_on_merge_request, author) + + should_create_todo(user: john_doe, target: mr_unassigned, author: author, action: Todo::DIRECTLY_ADDRESSED, note: addressed_diff_note_on_merge_request) + end + it 'creates a todo for mentioned user on legacy diff note' do service.new_note(legacy_diff_note_on_merge_request, author) diff --git a/spec/services/users/destroy_spec.rb b/spec/services/users/destroy_spec.rb index 46e58393218..922e82445d0 100644 --- a/spec/services/users/destroy_spec.rb +++ b/spec/services/users/destroy_spec.rb @@ -2,11 +2,11 @@ require 'spec_helper' describe Users::DestroyService, services: true do describe "Deletes a user and all their personal projects" do - let!(:user) { create(:user) } - let!(:current_user) { create(:user) } - let!(:namespace) { create(:namespace, owner: user) } - let!(:project) { create(:project, namespace: namespace) } - let(:service) { described_class.new(current_user) } + let!(:user) { create(:user) } + let!(:admin) { create(:admin) } + let!(:namespace) { create(:namespace, owner: user) } + let!(:project) { create(:project, namespace: namespace) } + let(:service) { described_class.new(admin) } context 'no options are given' do it 'deletes the user' do @@ -24,6 +24,54 @@ describe Users::DestroyService, services: true do end end + context "a deleted user's issues" do + let(:project) { create :project } + + before do + project.add_developer(user) + end + + context "for an issue the user has created" do + let!(:issue) { create(:issue, project: project, author: user) } + + before do + service.execute(user) + end + + it 'does not delete the issue' do + expect(Issue.find_by_id(issue.id)).to be_present + end + + it 'migrates the issue so that the "Ghost User" is the issue owner' do + migrated_issue = Issue.find_by_id(issue.id) + + expect(migrated_issue.author).to eq(User.ghost) + end + + it 'blocks the user before migrating issues to the "Ghost User' do + expect(user).to be_blocked + end + end + + context "for an issue the user was assigned to" do + let!(:issue) { create(:issue, project: project, assignee: user) } + + before do + service.execute(user) + end + + it 'does not delete issues the user is assigned to' do + expect(Issue.find_by_id(issue.id)).to be_present + end + + it 'migrates the issue so that it is "Unassigned"' do + migrated_issue = Issue.find_by_id(issue.id) + + expect(migrated_issue.assignee).to be_nil + end + end + end + context "solo owned groups present" do let(:solo_owned) { create(:group) } let(:member) { create(:group_member) } @@ -57,5 +105,26 @@ describe Users::DestroyService, services: true do expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound) end end + + context "deletion permission checks" do + it 'does not delete the user when user is not an admin' do + other_user = create(:user) + + expect { described_class.new(other_user).execute(user) }.to raise_error(Gitlab::Access::AccessDeniedError) + expect(User.exists?(user.id)).to be(true) + end + + it 'allows admins to delete anyone' do + described_class.new(admin).execute(user) + + expect(User.exists?(user.id)).to be(false) + end + + it 'allows users to delete their own account' do + described_class.new(user).execute(user) + + expect(User.exists?(user.id)).to be(false) + end + end end end diff --git a/spec/services/users/refresh_authorized_projects_service_spec.rb b/spec/services/users/refresh_authorized_projects_service_spec.rb index 690fe979492..08733d6dcf1 100644 --- a/spec/services/users/refresh_authorized_projects_service_spec.rb +++ b/spec/services/users/refresh_authorized_projects_service_spec.rb @@ -131,6 +131,80 @@ describe Users::RefreshAuthorizedProjectsService do it 'sets the values to the access levels' do expect(hash.values).to eq([Gitlab::Access::MASTER]) end + + context 'personal projects' do + it 'includes the project with the right access level' do + expect(hash[project.id]).to eq(Gitlab::Access::MASTER) + end + end + + context 'projects the user is a member of' do + let!(:other_project) { create(:empty_project) } + + before do + other_project.team.add_reporter(user) + end + + it 'includes the project with the right access level' do + expect(hash[other_project.id]).to eq(Gitlab::Access::REPORTER) + end + end + + context 'projects of groups the user is a member of' do + let(:group) { create(:group) } + let!(:other_project) { create(:project, group: group) } + + before do + group.add_owner(user) + end + + it 'includes the project with the right access level' do + expect(hash[other_project.id]).to eq(Gitlab::Access::OWNER) + end + end + + context 'projects of subgroups of groups the user is a member of' do + let(:group) { create(:group) } + let(:nested_group) { create(:group, parent: group) } + let!(:other_project) { create(:project, group: nested_group) } + + before do + group.add_master(user) + end + + it 'includes the project with the right access level' do + expect(hash[other_project.id]).to eq(Gitlab::Access::MASTER) + end + end + + context 'projects shared with groups the user is a member of' do + let(:group) { create(:group) } + let(:other_project) { create(:empty_project) } + let!(:project_group_link) { create(:project_group_link, project: other_project, group: group, group_access: Gitlab::Access::GUEST) } + + before do + group.add_master(user) + end + + it 'includes the project with the right access level' do + expect(hash[other_project.id]).to eq(Gitlab::Access::GUEST) + end + end + + context 'projects shared with subgroups of groups the user is a member of' do + let(:group) { create(:group) } + let(:nested_group) { create(:group, parent: group) } + let(:other_project) { create(:empty_project) } + let!(:project_group_link) { create(:project_group_link, project: other_project, group: nested_group, group_access: Gitlab::Access::DEVELOPER) } + + before do + group.add_master(user) + end + + it 'includes the project with the right access level' do + expect(hash[other_project.id]).to eq(Gitlab::Access::DEVELOPER) + end + end end describe '#current_authorizations_per_project' do diff --git a/spec/services/wiki_pages/create_service_spec.rb b/spec/services/wiki_pages/create_service_spec.rb new file mode 100644 index 00000000000..5341ba3d261 --- /dev/null +++ b/spec/services/wiki_pages/create_service_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe WikiPages::CreateService, services: true do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + let(:opts) do + { + title: 'Title', + content: 'Content for wiki page', + format: 'markdown' + } + end + let(:service) { described_class.new(project, user, opts) } + + describe '#execute' do + context "valid params" do + before do + allow(service).to receive(:execute_hooks) + project.add_master(user) + end + + subject { service.execute } + + it 'creates a valid wiki page' do + is_expected.to be_valid + expect(subject.title).to eq(opts[:title]) + expect(subject.content).to eq(opts[:content]) + expect(subject.format).to eq(opts[:format].to_sym) + end + + it 'executes webhooks' do + expect(service).to have_received(:execute_hooks).once.with(subject, 'create') + end + end + end +end diff --git a/spec/services/wiki_pages/destroy_service_spec.rb b/spec/services/wiki_pages/destroy_service_spec.rb new file mode 100644 index 00000000000..a4b9a390fe2 --- /dev/null +++ b/spec/services/wiki_pages/destroy_service_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe WikiPages::DestroyService, services: true do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + let(:wiki_page) { create(:wiki_page) } + let(:service) { described_class.new(project, user) } + + describe '#execute' do + before do + allow(service).to receive(:execute_hooks) + project.add_master(user) + end + + it 'executes webhooks' do + service.execute(wiki_page) + + expect(service).to have_received(:execute_hooks).once.with(wiki_page, 'delete') + end + end +end diff --git a/spec/services/wiki_pages/update_service_spec.rb b/spec/services/wiki_pages/update_service_spec.rb new file mode 100644 index 00000000000..2bccca764d7 --- /dev/null +++ b/spec/services/wiki_pages/update_service_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe WikiPages::UpdateService, services: true do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + let(:wiki_page) { create(:wiki_page) } + let(:opts) do + { + content: 'New content for wiki page', + format: 'markdown', + message: 'New wiki message' + } + end + let(:service) { described_class.new(project, user, opts) } + + describe '#execute' do + context "valid params" do + before do + allow(service).to receive(:execute_hooks) + project.add_master(user) + end + + subject { service.execute(wiki_page) } + + it 'updates the wiki page' do + is_expected.to be_valid + expect(subject.content).to eq(opts[:content]) + expect(subject.format).to eq(opts[:format].to_sym) + expect(subject.message).to eq(opts[:message]) + end + + it 'executes webhooks' do + expect(service).to have_received(:execute_hooks).once.with(subject, 'update') + end + end + end +end diff --git a/spec/support/api/pagination_shared_examples.rb b/spec/support/api/pagination_shared_examples.rb deleted file mode 100644 index 352a6eeec79..00000000000 --- a/spec/support/api/pagination_shared_examples.rb +++ /dev/null @@ -1,20 +0,0 @@ -# Specs for paginated resources. -# -# Requires an API request: -# let(:request) { get api("/projects/#{project.id}/repository/branches", user) } -shared_examples 'a paginated resources' do - before do - # Fires the request - request - end - - it 'has pagination headers' do - expect(response.headers).to include('X-Total') - expect(response.headers).to include('X-Total-Pages') - expect(response.headers).to include('X-Per-Page') - expect(response.headers).to include('X-Page') - expect(response.headers).to include('X-Next-Page') - expect(response.headers).to include('X-Prev-Page') - expect(response.headers).to include('Link') - end -end diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb index 6ed55289ed9..c864a705ca4 100644 --- a/spec/support/cycle_analytics_helpers.rb +++ b/spec/support/cycle_analytics_helpers.rb @@ -9,14 +9,7 @@ module CycleAnalyticsHelpers commit_shas = Array.new(count) do |index| filename = random_git_name - options = { - committer: project.repository.user_to_committer(user), - author: project.repository.user_to_committer(user), - commit: { message: message, branch: branch_name, update_ref: true }, - file: { content: "content", path: filename, update: false } - } - - commit_sha = Gitlab::Git::Blob.commit(project.repository, options) + commit_sha = project.repository.create_file(user, filename, "content", message: message, branch_name: branch_name) project.repository.commit(commit_sha) commit_sha @@ -35,13 +28,12 @@ module CycleAnalyticsHelpers project.repository.add_branch(user, source_branch, 'master') end - sha = project.repository.commit_file( + sha = project.repository.create_file( user, random_git_name, 'content', message: 'commit message', - branch_name: source_branch, - update: false) + branch_name: source_branch) project.repository.commit(sha) opts = { diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb index 247f0954221..6f31828b825 100644 --- a/spec/support/db_cleaner.rb +++ b/spec/support/db_cleaner.rb @@ -3,6 +3,10 @@ RSpec.configure do |config| DatabaseCleaner.clean_with(:truncation) end + config.append_after(:context) do + DatabaseCleaner.clean_with(:truncation) + end + config.before(:each) do DatabaseCleaner.strategy = :transaction end diff --git a/spec/support/dropzone_helper.rb b/spec/support/dropzone_helper.rb new file mode 100644 index 00000000000..984ec7d2741 --- /dev/null +++ b/spec/support/dropzone_helper.rb @@ -0,0 +1,37 @@ +module DropzoneHelper + # Provides a way to perform `attach_file` for a Dropzone-based file input + # + # This is accomplished by creating a standard HTML file input on the page, + # performing `attach_file` on that field, and then triggering the appropriate + # Dropzone events to perform the actual upload. + # + # This method waits for the upload to complete before returning. + def dropzone_file(file_path) + # Generate a fake file input that Capybara can attach to + page.execute_script <<-JS.strip_heredoc + var fakeFileInput = window.$('<input/>').attr( + {id: 'fakeFileInput', type: 'file'} + ).appendTo('body'); + + window._dropzoneComplete = false; + JS + + # Attach the file to the fake input selector with Capybara + attach_file('fakeFileInput', file_path) + + # Manually trigger a Dropzone "drop" event with the fake input's file list + page.execute_script <<-JS.strip_heredoc + var fileList = [$('#fakeFileInput')[0].files[0]]; + var e = jQuery.Event('drop', { dataTransfer : { files : fileList } }); + + var dropzone = $('.div-dropzone')[0].dropzone; + dropzone.on('queuecomplete', function() { + window._dropzoneComplete = true; + }); + dropzone.listeners[0].events.drop(e); + JS + + # Wait until Dropzone's fired `queuecomplete` + loop until page.evaluate_script('window._dropzoneComplete === true') + end +end diff --git a/spec/support/features/rss_shared_examples.rb b/spec/support/features/rss_shared_examples.rb new file mode 100644 index 00000000000..9a3b0a731ad --- /dev/null +++ b/spec/support/features/rss_shared_examples.rb @@ -0,0 +1,23 @@ +shared_examples "an autodiscoverable RSS feed with current_user's private token" do + it "has an RSS autodiscovery link tag with current_user's private token" do + expect(page).to have_css("link[type*='atom+xml'][href*='private_token=#{Thread.current[:current_user].private_token}']", visible: false) + end +end + +shared_examples "it has an RSS button with current_user's private token" do + it "shows the RSS button with current_user's private token" do + expect(page).to have_css("a:has(.fa-rss)[href*='private_token=#{Thread.current[:current_user].private_token}']") + end +end + +shared_examples "an autodiscoverable RSS feed without a private token" do + it "has an RSS autodiscovery link tag without a private token" do + expect(page).to have_css("link[type*='atom+xml']:not([href*='private_token'])", visible: false) + end +end + +shared_examples "it has an RSS button without a private token" do + it "shows the RSS button without a private token" do + expect(page).to have_css("a:has(.fa-rss):not([href*='private_token'])") + end +end diff --git a/spec/support/filtered_search_helpers.rb b/spec/support/filtered_search_helpers.rb new file mode 100644 index 00000000000..58f6636e680 --- /dev/null +++ b/spec/support/filtered_search_helpers.rb @@ -0,0 +1,37 @@ +module FilteredSearchHelpers + def filtered_search + page.find('.filtered-search') + end + + def input_filtered_search(search_term, submit: true) + filtered_search.set(search_term) + + if submit + filtered_search.send_keys(:enter) + end + end + + def input_filtered_search_keys(search_term) + filtered_search.send_keys(search_term) + filtered_search.send_keys(:enter) + end + + def expect_filtered_search_input(input) + expect(find('.filtered-search').value).to eq(input) + end + + def clear_search_field + find('.filtered-search-input-container .clear-search').click + end + + def reset_filters + clear_search_field + filtered_search.send_keys(:enter) + end + + def init_label_search + filtered_search.set('label:') + # This ensures the dropdown is shown + expect(find('#js-dropdown-label')).not_to have_css('.filter-dropdown-loading') + end +end diff --git a/spec/support/gitlab_stubs/session.json b/spec/support/gitlab_stubs/session.json index ce8dfe5ae75..cd55d63125e 100644 --- a/spec/support/gitlab_stubs/session.json +++ b/spec/support/gitlab_stubs/session.json @@ -7,7 +7,7 @@ "skype":"aertert", "linkedin":"", "twitter":"", - "theme_id":2,"color_scheme_id":2, + "color_scheme_id":2, "state":"active", "created_at":"2012-12-21T13:02:20Z", "extern_uid":null, @@ -17,4 +17,4 @@ "can_create_project":false, "private_token":"Wvjy2Krpb7y8xi93owUz", "access_token":"Wvjy2Krpb7y8xi93owUz" -}
\ No newline at end of file +} diff --git a/spec/support/gitlab_stubs/user.json b/spec/support/gitlab_stubs/user.json index ce8dfe5ae75..cd55d63125e 100644 --- a/spec/support/gitlab_stubs/user.json +++ b/spec/support/gitlab_stubs/user.json @@ -7,7 +7,7 @@ "skype":"aertert", "linkedin":"", "twitter":"", - "theme_id":2,"color_scheme_id":2, + "color_scheme_id":2, "state":"active", "created_at":"2012-12-21T13:02:20Z", "extern_uid":null, @@ -17,4 +17,4 @@ "can_create_project":false, "private_token":"Wvjy2Krpb7y8xi93owUz", "access_token":"Wvjy2Krpb7y8xi93owUz" -}
\ No newline at end of file +} diff --git a/spec/support/issuables_list_metadata_shared_examples.rb b/spec/support/issuables_list_metadata_shared_examples.rb new file mode 100644 index 00000000000..4c0f556e736 --- /dev/null +++ b/spec/support/issuables_list_metadata_shared_examples.rb @@ -0,0 +1,36 @@ +shared_examples 'issuables list meta-data' do |issuable_type, action = nil| + before do + @issuable_ids = [] + + 2.times do + issuable = + if issuable_type == :issue + create(issuable_type, project: project) + else + create(issuable_type, title: FFaker::Lorem.sentence, source_project: project, source_branch: FFaker::Name.name) + end + + @issuable_ids << issuable.id + + issuable.id.times { create(:note, noteable: issuable, project: issuable.project) } + (issuable.id + 1).times { create(:award_emoji, :downvote, awardable: issuable) } + (issuable.id + 2).times { create(:award_emoji, :upvote, awardable: issuable) } + end + end + + it "creates indexed meta-data object for issuable notes and votes count" do + if action + get action + else + get :index, namespace_id: project.namespace, project_id: project + end + + meta_data = assigns(:issuable_meta_data) + + @issuable_ids.each do |id| + expect(meta_data[id].notes_count).to eq(id) + expect(meta_data[id].downvotes).to eq(id + 1) + expect(meta_data[id].upvotes).to eq(id + 2) + end + end +end diff --git a/spec/support/javascript_fixtures_helpers.rb b/spec/support/javascript_fixtures_helpers.rb index 0b8729db0f9..a982b159b48 100644 --- a/spec/support/javascript_fixtures_helpers.rb +++ b/spec/support/javascript_fixtures_helpers.rb @@ -5,7 +5,7 @@ require 'gitlab/popen' module JavaScriptFixturesHelpers include Gitlab::Popen - FIXTURE_PATH = 'spec/javascripts/fixtures' + FIXTURE_PATH = 'spec/javascripts/fixtures'.freeze # Public: Removes all fixture files from given directory # diff --git a/spec/support/jira_service_helper.rb b/spec/support/jira_service_helper.rb index 929fc0c5182..97ae0b6afc5 100644 --- a/spec/support/jira_service_helper.rb +++ b/spec/support/jira_service_helper.rb @@ -1,5 +1,5 @@ module JiraServiceHelper - JIRA_URL = "http://jira.example.net" + JIRA_URL = "http://jira.example.net".freeze JIRA_API = JIRA_URL + "/rest/api/2" def jira_service_settings diff --git a/spec/support/kubernetes_helpers.rb b/spec/support/kubernetes_helpers.rb index 444612cf871..b5ed71ba3be 100644 --- a/spec/support/kubernetes_helpers.rb +++ b/spec/support/kubernetes_helpers.rb @@ -2,23 +2,24 @@ module KubernetesHelpers include Gitlab::Kubernetes def kube_discovery_body - { "kind" => "APIResourceList", + { + "kind" => "APIResourceList", "resources" => [ { "name" => "pods", "namespaced" => true, "kind" => "Pod" }, - ], + ] } end def kube_pods_body(*pods) { "kind" => "PodList", - "items" => [ kube_pod ], - } + "items" => [kube_pod] } end # This is a partial response, it will have many more elements in reality but # these are the ones we care about at the moment def kube_pod(app: "valid-pod-label") - { "metadata" => { + { + "metadata" => { "name" => "kube-pod", "creationTimestamp" => "2016-11-25T19:55:19Z", "labels" => { "app" => app }, @@ -29,7 +30,7 @@ module KubernetesHelpers { "name" => "container-1" }, ], }, - "status" => { "phase" => "Running" }, + "status" => { "phase" => "Running" } } end diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb index ad1eed5b369..9ffb00be0b8 100644 --- a/spec/support/login_helpers.rb +++ b/spec/support/login_helpers.rb @@ -15,11 +15,12 @@ module LoginHelpers # user = create(:user) # login_as(user) def login_as(user_or_role) - if user_or_role.kind_of?(User) - @user = user_or_role - else - @user = create(user_or_role) - end + @user = + if user_or_role.is_a?(User) + user_or_role + else + create(user_or_role) + end login_with(@user) end diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb index a79386b5db9..dea0015f105 100644 --- a/spec/support/markdown_feature.rb +++ b/spec/support/markdown_feature.rb @@ -79,8 +79,8 @@ class MarkdownFeature def xproject @xproject ||= begin - namespace = create(:namespace, name: 'cross-reference') - create(:project, namespace: namespace) do |project| + group = create(:group, :nested) + create(:project, namespace: group) do |project| project.team << [user, :developer] end end diff --git a/spec/support/matchers/access_matchers.rb b/spec/support/matchers/access_matchers.rb index ceddb656596..7d238850520 100644 --- a/spec/support/matchers/access_matchers.rb +++ b/spec/support/matchers/access_matchers.rb @@ -38,7 +38,7 @@ module AccessMatchers end def description_for(user, type) - if user.kind_of?(User) + if user.is_a?(User) # User#inspect displays too much information for RSpec's descriptions "be #{type} for the specified user" else diff --git a/spec/support/matchers/gitaly_matchers.rb b/spec/support/matchers/gitaly_matchers.rb new file mode 100644 index 00000000000..d7a53820684 --- /dev/null +++ b/spec/support/matchers/gitaly_matchers.rb @@ -0,0 +1,3 @@ +RSpec::Matchers.define :post_receive_request_with_repo_path do |path| + match { |actual| actual.repository.path == path } +end diff --git a/spec/support/matchers/pagination_matcher.rb b/spec/support/matchers/pagination_matcher.rb new file mode 100644 index 00000000000..60f5e8239a7 --- /dev/null +++ b/spec/support/matchers/pagination_matcher.rb @@ -0,0 +1,5 @@ +RSpec::Matchers.define :include_pagination_headers do |expected| + match do |actual| + expect(actual.headers).to include('X-Total', 'X-Total-Pages', 'X-Per-Page', 'X-Page', 'X-Next-Page', 'X-Prev-Page', 'Link') + end +end diff --git a/spec/support/merge_request_helpers.rb b/spec/support/merge_request_helpers.rb index d5801c8272f..326b85eabd0 100644 --- a/spec/support/merge_request_helpers.rb +++ b/spec/support/merge_request_helpers.rb @@ -10,4 +10,13 @@ module MergeRequestHelpers def last_merge_request page.all('ul.mr-list > li').last.text end + + def expect_mr_list_count(open_count, closed_count = 0) + all_count = open_count + closed_count + + expect(page).to have_issuable_counts(open: open_count, closed: closed_count, all: all_count) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: open_count) + end + end end diff --git a/spec/support/project_features_apply_to_issuables_shared_examples.rb b/spec/support/project_features_apply_to_issuables_shared_examples.rb index 4621d17549b..f8b7d0527ba 100644 --- a/spec/support/project_features_apply_to_issuables_shared_examples.rb +++ b/spec/support/project_features_apply_to_issuables_shared_examples.rb @@ -18,7 +18,7 @@ shared_examples 'project features apply to issuables' do |klass| before do _ = issuable - login_as(user) + login_as(user) if user visit path end diff --git a/spec/support/repo_helpers.rb b/spec/support/repo_helpers.rb index 73f375c481b..e9d5c7b12ae 100644 --- a/spec/support/repo_helpers.rb +++ b/spec/support/repo_helpers.rb @@ -42,7 +42,7 @@ Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> eos ) end - + def another_sample_commit OpenStruct.new( id: "e56497bb5f03a90a51293fc6d516788730953899", @@ -100,13 +100,13 @@ eos } ] - commits = [ - '5937ac0a7beb003549fc5fd26fc247adbce4a52e', - '570e7b2abdd848b95f2f578043fc23bd6f6fd24d', - '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9', - 'd14d6c0abdd253381df51a723d58691b2ee1ab08', - 'c1acaa58bbcbc3eafe538cb8274ba387047b69f8', - ].reverse # last commit is recent one + commits = %w( + 5937ac0a7beb003549fc5fd26fc247adbce4a52e + 570e7b2abdd848b95f2f578043fc23bd6f6fd24d + 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + d14d6c0abdd253381df51a723d58691b2ee1ab08 + c1acaa58bbcbc3eafe538cb8274ba387047b69f8 + ).reverse # last commit is recent one OpenStruct.new( source_branch: 'master', diff --git a/spec/support/seed_helper.rb b/spec/support/seed_helper.rb index 03fa0a66b9a..07f81e9c4f3 100644 --- a/spec/support/seed_helper.rb +++ b/spec/support/seed_helper.rb @@ -7,7 +7,7 @@ TEST_MUTABLE_REPO_PATH = File.join(SEED_REPOSITORY_PATH, "mutable-repo.git") TEST_BROKEN_REPO_PATH = File.join(SEED_REPOSITORY_PATH, "broken-repo.git") module SeedHelper - GITLAB_URL = "https://gitlab.com/gitlab-org/gitlab-git-test.git" + GITLAB_URL = "https://gitlab.com/gitlab-org/gitlab-git-test.git".freeze def ensure_seeds if File.exist?(SEED_REPOSITORY_PATH) diff --git a/spec/support/seed_repo.rb b/spec/support/seed_repo.rb index 9f2cd7c67c5..99a500bbbb1 100644 --- a/spec/support/seed_repo.rb +++ b/spec/support/seed_repo.rb @@ -25,64 +25,64 @@ module SeedRepo module BigCommit - ID = "913c66a37b4a45b9769037c55c2d238bd0942d2e" - PARENT_ID = "cfe32cf61b73a0d5e9f13e774abde7ff789b1660" - MESSAGE = "Files, encoding and much more" - AUTHOR_FULL_NAME = "Dmitriy Zaporozhets" + ID = "913c66a37b4a45b9769037c55c2d238bd0942d2e".freeze + PARENT_ID = "cfe32cf61b73a0d5e9f13e774abde7ff789b1660".freeze + MESSAGE = "Files, encoding and much more".freeze + AUTHOR_FULL_NAME = "Dmitriy Zaporozhets".freeze FILES_COUNT = 2 end module Commit - ID = "570e7b2abdd848b95f2f578043fc23bd6f6fd24d" - PARENT_ID = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9" - MESSAGE = "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n" - AUTHOR_FULL_NAME = "Dmitriy Zaporozhets" - FILES = ["files/ruby/popen.rb", "files/ruby/regex.rb"] + ID = "570e7b2abdd848b95f2f578043fc23bd6f6fd24d".freeze + PARENT_ID = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9".freeze + MESSAGE = "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n".freeze + AUTHOR_FULL_NAME = "Dmitriy Zaporozhets".freeze + FILES = ["files/ruby/popen.rb", "files/ruby/regex.rb"].freeze FILES_COUNT = 2 - C_FILE_PATH = "files/ruby" - C_FILES = ["popen.rb", "regex.rb", "version_info.rb"] - BLOB_FILE = %{%h3= @key.title\n%hr\n%pre= @key.key\n.actions\n = link_to 'Remove', @key, :confirm => 'Are you sure?', :method => :delete, :class => \"btn danger delete-key\"\n\n\n} - BLOB_FILE_PATH = "app/views/keys/show.html.haml" + C_FILE_PATH = "files/ruby".freeze + C_FILES = ["popen.rb", "regex.rb", "version_info.rb"].freeze + BLOB_FILE = %{%h3= @key.title\n%hr\n%pre= @key.key\n.actions\n = link_to 'Remove', @key, :confirm => 'Are you sure?', :method => :delete, :class => \"btn danger delete-key\"\n\n\n}.freeze + BLOB_FILE_PATH = "app/views/keys/show.html.haml".freeze end module EmptyCommit - ID = "b0e52af38d7ea43cf41d8a6f2471351ac036d6c9" - PARENT_ID = "40f4a7a617393735a95a0bb67b08385bc1e7c66d" - MESSAGE = "Empty commit" - AUTHOR_FULL_NAME = "Rémy Coutable" - FILES = [] + ID = "b0e52af38d7ea43cf41d8a6f2471351ac036d6c9".freeze + PARENT_ID = "40f4a7a617393735a95a0bb67b08385bc1e7c66d".freeze + MESSAGE = "Empty commit".freeze + AUTHOR_FULL_NAME = "Rémy Coutable".freeze + FILES = [].freeze FILES_COUNT = FILES.count end module EncodingCommit - ID = "40f4a7a617393735a95a0bb67b08385bc1e7c66d" - PARENT_ID = "66028349a123e695b589e09a36634d976edcc5e8" - MESSAGE = "Add ISO-8859-encoded file" - AUTHOR_FULL_NAME = "Stan Hu" - FILES = ["encoding/iso8859.txt"] + ID = "40f4a7a617393735a95a0bb67b08385bc1e7c66d".freeze + PARENT_ID = "66028349a123e695b589e09a36634d976edcc5e8".freeze + MESSAGE = "Add ISO-8859-encoded file".freeze + AUTHOR_FULL_NAME = "Stan Hu".freeze + FILES = ["encoding/iso8859.txt"].freeze FILES_COUNT = FILES.count end module FirstCommit - ID = "1a0b36b3cdad1d2ee32457c102a8c0b7056fa863" + ID = "1a0b36b3cdad1d2ee32457c102a8c0b7056fa863".freeze PARENT_ID = nil - MESSAGE = "Initial commit" - AUTHOR_FULL_NAME = "Dmitriy Zaporozhets" - FILES = ["LICENSE", ".gitignore", "README.md"] + MESSAGE = "Initial commit".freeze + AUTHOR_FULL_NAME = "Dmitriy Zaporozhets".freeze + FILES = ["LICENSE", ".gitignore", "README.md"].freeze FILES_COUNT = 3 end module LastCommit - ID = "4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6" - PARENT_ID = "0e1b353b348f8477bdbec1ef47087171c5032cd9" - MESSAGE = "Merge branch 'master' into 'master'" - AUTHOR_FULL_NAME = "Stan Hu" - FILES = ["bin/executable"] + ID = "4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6".freeze + PARENT_ID = "0e1b353b348f8477bdbec1ef47087171c5032cd9".freeze + MESSAGE = "Merge branch 'master' into 'master'".freeze + AUTHOR_FULL_NAME = "Stan Hu".freeze + FILES = ["bin/executable"].freeze FILES_COUNT = FILES.count end module Repo - HEAD = "master" + HEAD = "master".freeze BRANCHES = %w[ feature fix @@ -93,14 +93,14 @@ module SeedRepo gitattributes-updated master merge-test - ] - TAGS = %w[v1.0.0 v1.1.0 v1.2.0 v1.2.1] + ].freeze + TAGS = %w[v1.0.0 v1.1.0 v1.2.0 v1.2.1].freeze end module RubyBlob - ID = "7e3e39ebb9b2bf433b4ad17313770fbe4051649c" - NAME = "popen.rb" - CONTENT = <<-eos + ID = "7e3e39ebb9b2bf433b4ad17313770fbe4051649c".freeze + NAME = "popen.rb".freeze + CONTENT = <<-eos.freeze require 'fileutils' require 'open3' diff --git a/spec/support/select2_helper.rb b/spec/support/select2_helper.rb index d30cc8ff9f2..0d526045012 100644 --- a/spec/support/select2_helper.rb +++ b/spec/support/select2_helper.rb @@ -12,7 +12,7 @@ module Select2Helper def select2(value, options = {}) - raise ArgumentError, 'options must be a Hash' unless options.kind_of?(Hash) + raise ArgumentError, 'options must be a Hash' unless options.is_a?(Hash) selector = options.fetch(:from) diff --git a/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb index dd54b0addda..81d06dc2a3d 100644 --- a/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb +++ b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb @@ -11,7 +11,7 @@ shared_examples 'new issuable record that supports slash commands' do let(:params) { base_params.merge(defined?(default_params) ? default_params : {}).merge(example_params) } let(:issuable) { described_class.new(project, user, params).execute } - before { project.team << [assignee, :master ] } + before { project.team << [assignee, :master] } context 'with labels in command only' do let(:example_params) do @@ -58,7 +58,7 @@ shared_examples 'new issuable record that supports slash commands' do let(:example_params) do { assignee: create(:user), - milestone_id: double(:milestone), + milestone_id: 1, description: %(/assign @#{assignee.username}\n/milestone %"#{milestone.name}") } end diff --git a/spec/support/slash_commands_helpers.rb b/spec/support/slash_commands_helpers.rb index df483afa0e3..0d91fe5fd5d 100644 --- a/spec/support/slash_commands_helpers.rb +++ b/spec/support/slash_commands_helpers.rb @@ -3,7 +3,7 @@ module SlashCommandsHelpers Sidekiq::Testing.fake! do page.within('.js-main-target-form') do fill_in 'note[note]', with: text - click_button 'Comment' + find('.comment-btn').trigger('click') end end end diff --git a/spec/support/stub_gitlab_calls.rb b/spec/support/stub_gitlab_calls.rb index 93f96cacc00..a01ef576234 100644 --- a/spec/support/stub_gitlab_calls.rb +++ b/spec/support/stub_gitlab_calls.rb @@ -35,7 +35,7 @@ module StubGitlabCalls { "tags" => tags } ) allow_any_instance_of(ContainerRegistry::Client).to receive(:repository_manifest).and_return( - JSON.load(File.read(Rails.root + 'spec/fixtures/container_registry/tag_manifest.json')) + JSON.parse(File.read(Rails.root + 'spec/fixtures/container_registry/tag_manifest.json')) ) allow_any_instance_of(ContainerRegistry::Client).to receive(:blob).and_return( File.read(Rails.root + 'spec/fixtures/container_registry/config_blob.json') diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index b87232a350b..c3aa3ef44c2 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -38,7 +38,7 @@ module TestEnv 'deleted-image-test' => '6c17798', 'wip' => 'b9238ee', 'csv' => '3dd0896' - } + }.freeze # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily # need to keep all the branches in sync. @@ -48,7 +48,7 @@ module TestEnv 'master' => '5937ac0', 'remove-submodule' => '2a33e0c', 'conflict-resolvable-fork' => '404fa3f' - } + }.freeze # Test environment # @@ -135,7 +135,7 @@ module TestEnv def copy_repo(project) base_repo_path = File.expand_path(factory_repo_path_bare) - target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.namespace.path}/#{project.path}.git") + target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.full_path}.git") FileUtils.mkdir_p(target_repo_path) FileUtils.cp_r("#{base_repo_path}/.", target_repo_path) FileUtils.chmod_R 0755, target_repo_path @@ -152,7 +152,7 @@ module TestEnv def copy_forked_repo_with_submodules(project) base_repo_path = File.expand_path(forked_repo_path_bare) - target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.namespace.path}/#{project.path}.git") + target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.full_path}.git") FileUtils.mkdir_p(target_repo_path) FileUtils.cp_r("#{base_repo_path}/.", target_repo_path) FileUtils.chmod_R 0755, target_repo_path diff --git a/spec/support/time_tracking_shared_examples.rb b/spec/support/time_tracking_shared_examples.rb index 02657684b57..52f4fabdc47 100644 --- a/spec/support/time_tracking_shared_examples.rb +++ b/spec/support/time_tracking_shared_examples.rb @@ -77,6 +77,6 @@ end def submit_time(slash_command) fill_in 'note[note]', with: slash_command - click_button 'Comment' + find('.comment-btn').trigger('click') wait_for_ajax end diff --git a/spec/support/update_invalid_issuable.rb b/spec/support/update_invalid_issuable.rb new file mode 100644 index 00000000000..365c34448ac --- /dev/null +++ b/spec/support/update_invalid_issuable.rb @@ -0,0 +1,57 @@ +shared_examples 'update invalid issuable' do |klass| + let(:params) do + { + namespace_id: project.namespace.path, + project_id: project.path, + id: issuable.iid + } + end + + let(:issuable) do + klass == Issue ? issue : merge_request + end + + before do + if klass == Issue + params.merge!(issue: { title: "any" }) + else + params.merge!(merge_request: { title: "any" }) + end + end + + context 'when updating causes conflicts' do + before do + allow_any_instance_of(issuable.class).to receive(:save). + and_raise(ActiveRecord::StaleObjectError.new(issuable, :save)) + end + + it 'renders edit when format is html' do + put :update, params + + expect(response).to render_template(:edit) + expect(assigns[:conflict]).to be_truthy + end + + it 'renders json error message when format is json' do + params[:format] = "json" + + put :update, params + + expect(response.status).to eq(409) + expect(JSON.parse(response.body)).to have_key('errors') + end + end + + context 'when updating an invalid issuable' do + before do + key = klass == Issue ? :issue : :merge_request + params[key][:title] = "" + end + + it 'renders edit when merge request is invalid' do + put :update, params + + expect(response).to render_template(:edit) + end + end +end diff --git a/spec/uploaders/attachment_uploader_spec.rb b/spec/uploaders/attachment_uploader_spec.rb index 6098be5cd45..ea714fb08f0 100644 --- a/spec/uploaders/attachment_uploader_spec.rb +++ b/spec/uploaders/attachment_uploader_spec.rb @@ -1,18 +1,17 @@ require 'spec_helper' describe AttachmentUploader do - let(:issue) { build(:issue) } - subject { described_class.new(issue) } + let(:uploader) { described_class.new(build_stubbed(:user)) } describe '#move_to_cache' do it 'is true' do - expect(subject.move_to_cache).to eq(true) + expect(uploader.move_to_cache).to eq(true) end end describe '#move_to_store' do it 'is true' do - expect(subject.move_to_store).to eq(true) + expect(uploader.move_to_store).to eq(true) end end end diff --git a/spec/uploaders/avatar_uploader_spec.rb b/spec/uploaders/avatar_uploader_spec.rb index 76f5a4b42ed..c4d558805ab 100644 --- a/spec/uploaders/avatar_uploader_spec.rb +++ b/spec/uploaders/avatar_uploader_spec.rb @@ -1,18 +1,17 @@ require 'spec_helper' describe AvatarUploader do - let(:user) { build(:user) } - subject { described_class.new(user) } + let(:uploader) { described_class.new(build_stubbed(:user)) } describe '#move_to_cache' do it 'is false' do - expect(subject.move_to_cache).to eq(false) + expect(uploader.move_to_cache).to eq(false) end end describe '#move_to_store' do it 'is false' do - expect(subject.move_to_store).to eq(false) + expect(uploader.move_to_store).to eq(false) end end end diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb index 6a712e33c96..b0f5be55c33 100644 --- a/spec/uploaders/file_uploader_spec.rb +++ b/spec/uploaders/file_uploader_spec.rb @@ -1,57 +1,35 @@ require 'spec_helper' describe FileUploader do - let(:project) { create(:project) } + let(:uploader) { described_class.new(build_stubbed(:project)) } - before do - @previous_enable_processing = FileUploader.enable_processing - FileUploader.enable_processing = false - @uploader = FileUploader.new(project) - end - - after do - FileUploader.enable_processing = @previous_enable_processing - @uploader.remove! - end + describe 'initialize' do + it 'generates a secret if none is provided' do + expect(SecureRandom).to receive(:hex).and_return('secret') - describe '#image_or_video?' do - context 'given an image file' do - before do - @uploader.store!(fixture_file_upload(Rails.root.join('spec', 'fixtures', 'rails_sample.jpg'))) - end + uploader = described_class.new(double) - it 'detects an image based on file extension' do - expect(@uploader.image_or_video?).to be true - end + expect(uploader.secret).to eq 'secret' end - context 'given an video file' do - before do - video_file = fixture_file_upload(Rails.root.join('spec', 'fixtures', 'video_sample.mp4')) - @uploader.store!(video_file) - end - - it 'detects a video based on file extension' do - expect(@uploader.image_or_video?).to be true - end - end + it 'accepts a secret parameter' do + expect(SecureRandom).not_to receive(:hex) - it 'does not return image_or_video? for other types' do - @uploader.store!(fixture_file_upload(Rails.root.join('spec', 'fixtures', 'doc_sample.txt'))) + uploader = described_class.new(double, 'secret') - expect(@uploader.image_or_video?).to be false + expect(uploader.secret).to eq 'secret' end end describe '#move_to_cache' do it 'is true' do - expect(@uploader.move_to_cache).to eq(true) + expect(uploader.move_to_cache).to eq(true) end end describe '#move_to_store' do it 'is true' do - expect(@uploader.move_to_store).to eq(true) + expect(uploader.move_to_store).to eq(true) end end end diff --git a/spec/uploaders/uploader_helper_spec.rb b/spec/uploaders/uploader_helper_spec.rb new file mode 100644 index 00000000000..e9efd13b9aa --- /dev/null +++ b/spec/uploaders/uploader_helper_spec.rb @@ -0,0 +1,35 @@ +require 'rails_helper' + +describe UploaderHelper do + class ExampleUploader < CarrierWave::Uploader::Base + include UploaderHelper + + storage :file + end + + def upload_fixture(filename) + fixture_file_upload(Rails.root.join('spec', 'fixtures', filename)) + end + + describe '#image_or_video?' do + let(:uploader) { ExampleUploader.new } + + it 'returns true for an image file' do + uploader.store!(upload_fixture('dk.png')) + + expect(uploader).to be_image_or_video + end + + it 'it returns true for a video file' do + uploader.store!(upload_fixture('video_sample.mp4')) + + expect(uploader).to be_image_or_video + end + + it 'returns false for other extensions' do + uploader.store!(upload_fixture('doc_sample.txt')) + + expect(uploader).not_to be_image_or_video + end + end +end diff --git a/spec/views/ci/status/_badge.html.haml_spec.rb b/spec/views/ci/status/_badge.html.haml_spec.rb new file mode 100644 index 00000000000..c62450fb8e2 --- /dev/null +++ b/spec/views/ci/status/_badge.html.haml_spec.rb @@ -0,0 +1,89 @@ +require 'spec_helper' + +describe 'ci/status/_badge', :view do + let(:user) { create(:user) } + let(:project) { create(:empty_project, :private) } + let(:pipeline) { create(:ci_pipeline, project: project) } + + context 'when rendering status for build' do + let(:build) do + create(:ci_build, :success, pipeline: pipeline) + end + + context 'when user has ability to see details' do + before do + project.add_developer(user) + end + + it 'has link to build details page' do + details_path = namespace_project_build_path( + project.namespace, project, build) + + render_status(build) + + expect(rendered).to have_link 'passed', href: details_path + end + end + + context 'when user do not have ability to see build details' do + before do + render_status(build) + end + + it 'contains build status text' do + expect(rendered).to have_content 'passed' + end + + it 'does not contain links' do + expect(rendered).not_to have_link 'passed' + end + end + end + + context 'when rendering status for external job' do + context 'when user has ability to see commit status details' do + before do + project.add_developer(user) + end + + context 'status has external target url' do + before do + external_job = create(:generic_commit_status, + status: :running, + pipeline: pipeline, + target_url: 'http://gitlab.com') + + render_status(external_job) + end + + it 'contains valid commit status text' do + expect(rendered).to have_content 'running' + end + + it 'has link to external status page' do + expect(rendered).to have_link 'running', href: 'http://gitlab.com' + end + end + + context 'status do not have external target url' do + before do + external_job = create(:generic_commit_status, status: :canceled) + + render_status(external_job) + end + + it 'contains valid commit status text' do + expect(rendered).to have_content 'canceled' + end + + it 'has link to external status page' do + expect(rendered).not_to have_link 'canceled' + end + end + end + end + + def render_status(resource) + render 'ci/status/badge', status: resource.detailed_status(user) + end +end diff --git a/spec/views/projects/_home_panel.html.haml_spec.rb b/spec/views/projects/_home_panel.html.haml_spec.rb new file mode 100644 index 00000000000..f5381a48207 --- /dev/null +++ b/spec/views/projects/_home_panel.html.haml_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe 'projects/_home_panel', :view do + let(:project) { create(:empty_project, :public) } + + let(:notification_settings) do + user&.notification_settings_for(project) + end + + before do + assign(:project, project) + assign(:notification_setting, notification_settings) + + allow(view).to receive(:current_user).and_return(user) + allow(view).to receive(:can?).and_return(false) + end + + context 'when user is signed in' do + let(:user) { create(:user) } + + it 'makes it possible to set notification level' do + render + + expect(view).to render_template('shared/notifications/_button') + expect(rendered).to have_selector('.notification-dropdown') + end + end + + context 'when user is signed out' do + let(:user) { nil } + + it 'is not possible to set notification level' do + render + + expect(rendered).not_to have_selector('.notification_dropdown') + end + end +end diff --git a/spec/views/projects/builds/show.html.haml_spec.rb b/spec/views/projects/builds/show.html.haml_spec.rb index b6f6e7b7a2b..ec78ac30593 100644 --- a/spec/views/projects/builds/show.html.haml_spec.rb +++ b/spec/views/projects/builds/show.html.haml_spec.rb @@ -209,6 +209,10 @@ describe 'projects/builds/show', :view do it 'does not show retry button' do expect(rendered).not_to have_link('Retry') end + + it 'does not show New issue button' do + expect(rendered).not_to have_link('New issue') + end end context 'when job is not running' do @@ -220,6 +224,23 @@ describe 'projects/builds/show', :view do it 'shows retry button' do expect(rendered).to have_link('Retry') end + + context 'if build passed' do + it 'does not show New issue button' do + expect(rendered).not_to have_link('New issue') + end + end + + context 'if build failed' do + before do + build.status = 'failed' + render + end + + it 'shows New issue button' do + expect(rendered).to have_link('New issue') + end + end end describe 'commit title in sidebar' do @@ -248,4 +269,25 @@ describe 'projects/builds/show', :view do expect(rendered).to have_css('.js-build-value', visible: false, text: 'TRIGGER_VALUE_2') end end + + describe 'New issue button' do + before do + build.status = 'failed' + render + end + + it 'links to issues/new with the title and description filled in' do + title = "Build Failed ##{build.id}" + build_url = namespace_project_build_url(project.namespace, project, build) + href = new_namespace_project_issue_path( + project.namespace, + project, + issue: { + title: title, + description: build_url + } + ) + expect(rendered).to have_link('New issue', href: href) + end + end end diff --git a/spec/views/projects/notes/_form.html.haml_spec.rb b/spec/views/projects/notes/_form.html.haml_spec.rb index b14b1ece2d0..b61f016967f 100644 --- a/spec/views/projects/notes/_form.html.haml_spec.rb +++ b/spec/views/projects/notes/_form.html.haml_spec.rb @@ -21,7 +21,7 @@ describe 'projects/notes/_form' do let(:note) { build(:"note_on_#{noteable}", project: project) } it 'says that only markdown is supported, not slash commands' do - expect(rendered).to have_content('Styling with Markdown and slash commands are supported') + expect(rendered).to have_content('Markdown and slash commands are supported') end end end @@ -30,7 +30,7 @@ describe 'projects/notes/_form' do let(:note) { build(:note_on_commit, project: project) } it 'says that only markdown is supported, not slash commands' do - expect(rendered).to have_content('Styling with Markdown is supported') + expect(rendered).to have_content('Markdown is supported') end end end diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb index 5ef8cf1105b..a60af574a08 100644 --- a/spec/workers/git_garbage_collect_worker_spec.rb +++ b/spec/workers/git_garbage_collect_worker_spec.rb @@ -102,8 +102,8 @@ describe GitGarbageCollectWorker do new_commit_sha = Rugged::Commit.create( rugged, message: "hello world #{SecureRandom.hex(6)}", - author: Gitlab::Git::committer_hash(email: 'foo@bar', name: 'baz'), - committer: Gitlab::Git::committer_hash(email: 'foo@bar', name: 'baz'), + author: Gitlab::Git.committer_hash(email: 'foo@bar', name: 'baz'), + committer: Gitlab::Git.committer_hash(email: 'foo@bar', name: 'baz'), tree: old_commit.tree, parents: [old_commit], ) diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb index 60605460adb..87521ae408e 100644 --- a/spec/workers/repository_fork_worker_spec.rb +++ b/spec/workers/repository_fork_worker_spec.rb @@ -15,24 +15,24 @@ describe RepositoryForkWorker do it "creates a new repository from a fork" do expect(shell).to receive(:fork_repository).with( '/test/path', - project.path_with_namespace, + project.full_path, project.repository_storage_path, - fork_project.namespace.path + fork_project.namespace.full_path ).and_return(true) subject.perform( project.id, '/test/path', - project.path_with_namespace, - fork_project.namespace.path) + project.full_path, + fork_project.namespace.full_path) end it 'flushes various caches' do expect(shell).to receive(:fork_repository).with( '/test/path', - project.path_with_namespace, + project.full_path, project.repository_storage_path, - fork_project.namespace.path + fork_project.namespace.full_path ).and_return(true) expect_any_instance_of(Repository).to receive(:expire_emptiness_caches). @@ -41,8 +41,8 @@ describe RepositoryForkWorker do expect_any_instance_of(Repository).to receive(:expire_exists_cache). and_call_original - subject.perform(project.id, '/test/path', project.path_with_namespace, - fork_project.namespace.path) + subject.perform(project.id, '/test/path', project.full_path, + fork_project.namespace.full_path) end it "handles bad fork" do @@ -53,8 +53,8 @@ describe RepositoryForkWorker do subject.perform( project.id, '/test/path', - project.path_with_namespace, - fork_project.namespace.path) + project.full_path, + fork_project.namespace.full_path) end end end diff --git a/spec/workers/repository_import_worker_spec.rb b/spec/workers/repository_import_worker_spec.rb index f1b1574abf4..c42f3147b7a 100644 --- a/spec/workers/repository_import_worker_spec.rb +++ b/spec/workers/repository_import_worker_spec.rb @@ -20,7 +20,7 @@ describe RepositoryImportWorker do context 'when the import has failed' do it 'hide the credentials that were used in the import URL' do - error = %Q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found } + error = %q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found } expect_any_instance_of(Projects::ImportService).to receive(:execute). and_return({ status: :error, message: error }) |