diff options
Diffstat (limited to 'spec/requests/api')
72 files changed, 6889 insertions, 672 deletions
diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb index 919c98d6437..46edbd49b28 100644 --- a/spec/requests/api/access_requests_spec.rb +++ b/spec/requests/api/access_requests_spec.rb @@ -200,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 @@ -210,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/api_internal_helpers_spec.rb b/spec/requests/api/api_internal_helpers_spec.rb index be4bc39ada2..f5265ea60ff 100644 --- a/spec/requests/api/api_internal_helpers_spec.rb +++ b/spec/requests/api/api_internal_helpers_spec.rb @@ -21,7 +21,7 @@ describe ::API::Helpers::InternalHelpers do # Relative and absolute storage paths, with and without trailing / ['.', './', Dir.pwd, Dir.pwd + '/'].each do |storage_path| context "storage path is #{storage_path}" do - subject { clean_project_path(project_path, [storage_path]) } + subject { clean_project_path(project_path, [{ 'path' => storage_path }]) } it { is_expected.to eq(expected) } end diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb index 6cc1ef315db..f4d4a8a2cc7 100644 --- a/spec/requests/api/award_emoji_spec.rb +++ b/spec/requests/api/award_emoji_spec.rb @@ -15,7 +15,7 @@ describe API::AwardEmoji, api: true do describe "GET /projects/:id/awardable/:awardable_id/award_emoji" do context 'on an issue' do it "returns an array of award_emoji" do - get api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user) + get api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user) expect(response).to have_http_status(200) expect(json_response).to be_an Array @@ -31,7 +31,7 @@ describe API::AwardEmoji, api: true do context 'on a merge request' do it "returns an array of award_emoji" do - get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji", user) + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/award_emoji", user) expect(response).to have_http_status(200) expect(response).to include_pagination_headers @@ -57,7 +57,7 @@ describe API::AwardEmoji, api: true do it 'returns a status code 404' do user1 = create(:user) - get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji", user1) + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/award_emoji", user1) expect(response).to have_http_status(404) end @@ -68,7 +68,7 @@ describe API::AwardEmoji, api: true do let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket') } it 'returns an array of award emoji' do - get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user) + get api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user) expect(response).to have_http_status(200) expect(json_response).to be_an Array @@ -79,7 +79,7 @@ describe API::AwardEmoji, api: true do describe "GET /projects/:id/awardable/:awardable_id/award_emoji/:award_id" do context 'on an issue' do it "returns the award emoji" do - get api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/#{award_emoji.id}", user) + get api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji/#{award_emoji.id}", user) expect(response).to have_http_status(200) expect(json_response['name']).to eq(award_emoji.name) @@ -88,7 +88,7 @@ describe API::AwardEmoji, api: true do end it "returns a 404 error if the award is not found" do - get api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/12345", user) + get api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji/12345", user) expect(response).to have_http_status(404) end @@ -96,7 +96,7 @@ describe API::AwardEmoji, api: true do context 'on a merge request' do it 'returns the award emoji' do - get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user) + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/award_emoji/#{downvote.id}", user) expect(response).to have_http_status(200) expect(json_response['name']).to eq(downvote.name) @@ -123,7 +123,7 @@ describe API::AwardEmoji, api: true do it 'returns a status code 404' do user1 = create(:user) - get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user1) + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/award_emoji/#{downvote.id}", user1) expect(response).to have_http_status(404) end @@ -134,7 +134,7 @@ describe API::AwardEmoji, api: true do let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket') } it 'returns an award emoji' do - get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji/#{rocket.id}", user) + get api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji/#{rocket.id}", user) expect(response).to have_http_status(200) expect(json_response).not_to be_an Array @@ -147,7 +147,7 @@ describe API::AwardEmoji, api: true do context "on an issue" do it "creates a new award emoji" do - post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'blowfish' + post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user), name: 'blowfish' expect(response).to have_http_status(201) expect(json_response['name']).to eq('blowfish') @@ -155,13 +155,13 @@ describe API::AwardEmoji, api: true do end it "returns a 400 bad request error if the name is not given" do - post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user) + post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user) expect(response).to have_http_status(400) end it "returns a 401 unauthorized error if the user is not authenticated" do - post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji"), name: 'thumbsup' + post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji"), name: 'thumbsup' expect(response).to have_http_status(401) end @@ -173,15 +173,15 @@ describe API::AwardEmoji, api: true do end it "normalizes +1 as thumbsup award" do - post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: '+1' + post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user), name: '+1' expect(issue.award_emoji.last.name).to eq("thumbsup") end context 'when the emoji already has been awarded' do it 'returns a 404 status code' do - post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'thumbsup' - post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'thumbsup' + post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user), name: 'thumbsup' + post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user), name: 'thumbsup' expect(response).to have_http_status(404) expect(json_response["message"]).to match("has already been taken") @@ -207,7 +207,7 @@ describe API::AwardEmoji, api: true do it 'creates a new award emoji' do expect do - post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket' + post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user), name: 'rocket' end.to change { note.award_emoji.count }.from(0).to(1) expect(response).to have_http_status(201) @@ -215,21 +215,21 @@ describe API::AwardEmoji, api: true do end it "it returns 404 error when user authored note" do - post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note2.id}/award_emoji", user), name: 'thumbsup' + post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note2.id}/award_emoji", user), name: 'thumbsup' expect(response).to have_http_status(404) end it "normalizes +1 as thumbsup award" do - post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: '+1' + post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user), name: '+1' expect(note.award_emoji.last.name).to eq("thumbsup") end context 'when the emoji already has been awarded' do it 'returns a 404 status code' do - post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket' - post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket' + post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user), name: 'rocket' + post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user), name: 'rocket' expect(response).to have_http_status(404) expect(json_response["message"]).to match("has already been taken") @@ -241,14 +241,14 @@ describe API::AwardEmoji, api: true do context 'when the awardable is an Issue' 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) + delete api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji/#{award_emoji.id}", user) - 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 - delete api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/12345", user) + delete api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji/12345", user) expect(response).to have_http_status(404) end @@ -257,14 +257,14 @@ describe API::AwardEmoji, api: true do context 'when the awardable is a Merge Request' 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) + delete api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/award_emoji/#{downvote.id}", user) - 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 - delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes/12345", user) + delete api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes/12345", user) expect(response).to have_http_status(404) end @@ -277,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,10 +289,10 @@ 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) + delete api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji/#{rocket.id}", user) - 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 71df534ebe1..87c36639cd4 100644 --- a/spec/requests/api/boards_spec.rb +++ b/spec/requests/api/boards_spec.rb @@ -195,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 5571f6cc107..a70f7beaae0 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -5,77 +5,146 @@ describe API::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(:guest) { create(:user).tap { |u| create(:project_member, :guest, user: u, project: project) } } let!(:branch_name) { 'feature' } let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } - let!(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master") } + let(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master")[:branch] } describe "GET /projects/:id/repository/branches" do - it "returns an array of project branches" do - project.repository.expire_all_method_caches + let(:route) { "/projects/#{project.id}/repository/branches" } - get api("/projects/#{project.id}/repository/branches", user), per_page: 100 + shared_examples_for 'repository branches' do + it 'returns the repository branches' do + get api(route, current_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) + 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) + end + + context 'when repository is disabled' do + include_context 'disabled repository' + + it_behaves_like '403 response' do + let(:request) { get api(route, current_user) } + end + end end - end - describe "GET /projects/:id/repository/branches/:branch" do - it "returns the branch information for a single branch" do - get api("/projects/#{project.id}/repository/branches/#{branch_name}", user) - expect(response).to have_http_status(200) + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository branches' do + let(:project) { create(:project, :public, :repository) } + let(:current_user) { nil } + end + end - expect(json_response['name']).to eq(branch_name) - 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) - expect(json_response['developers_can_merge']).to eq(false) + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get api(route) } + let(:message) { '404 Project Not Found' } + end end - it "returns the branch information for a single branch with dots in the name" do - get api("/projects/#{project.id}/repository/branches/with.1.2.3", user) + context 'when authenticated', 'as a developer' do + it_behaves_like 'repository branches' do + let(:current_user) { user } + end + end - expect(response).to have_http_status(200) - expect(json_response['name']).to eq("with.1.2.3") + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get api(route, guest) } + end end + end + + describe "GET /projects/:id/repository/branches/:branch" do + let(:route) { "/projects/#{project.id}/repository/branches/#{branch_name}" } - context 'on a merged branch' do - it "returns the branch information for a single branch" do - get api("/projects/#{project.id}/repository/branches/merge-test", user) + shared_examples_for 'repository branch' do |merged: false| + it 'returns the repository branch' do + get api(route, current_user) expect(response).to have_http_status(200) - expect(json_response['name']).to eq('merge-test') - expect(json_response['merged']).to eq(true) + expect(json_response['name']).to eq(branch_name) + expect(json_response['merged']).to eq(merged) + expect(json_response['protected']).to eq(false) + expect(json_response['developers_can_push']).to eq(false) + expect(json_response['developers_can_merge']).to eq(false) + + 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') + end + + context 'when branch does not exist' do + let(:branch_name) { 'unknown' } + + it_behaves_like '404 response' do + let(:request) { get api(route, current_user) } + let(:message) { '404 Branch Not Found' } + end + end + + context 'when repository is disabled' do + include_context 'disabled repository' + + it_behaves_like '403 response' do + let(:request) { get api(route, current_user) } + end end end - it "returns a 403 error if guest" do - get api("/projects/#{project.id}/repository/branches", user2) - expect(response).to have_http_status(403) + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository branch' do + let(:project) { create(:project, :public, :repository) } + let(:current_user) { nil } + end end - it "returns a 404 error if branch is not available" do - get api("/projects/#{project.id}/repository/branches/unknown", user) - expect(response).to have_http_status(404) + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get api(route) } + let(:message) { '404 Project Not Found' } + end + end + + context 'when authenticated', 'as a developer' do + let(:current_user) { user } + it_behaves_like 'repository branch' + + context 'when branch contains a dot' do + let(:branch_name) { branch_with_dot.name } + let(:branch_sha) { project.commit('master').sha } + + it_behaves_like 'repository branch' + end + + context 'when branch is merged' do + let(:branch_name) { 'merge-test' } + let(:branch_sha) { project.commit('merge-test').sha } + + it_behaves_like 'repository branch', merged: true + end + end + + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get api(route, guest) } + end end end @@ -93,10 +162,10 @@ describe API::Branches, api: true do end it "protects a single branch with dots in the name" do - put api("/projects/#{project.id}/repository/branches/with.1.2.3/protect", user) + put api("/projects/#{project.id}/repository/branches/#{branch_with_dot.name}/protect", user) expect(response).to have_http_status(200) - expect(json_response['name']).to eq("with.1.2.3") + expect(json_response['name']).to eq(branch_with_dot.name) expect(json_response['protected']).to eq(true) end @@ -234,7 +303,7 @@ describe API::Branches, api: true do end it "returns a 403 error if guest" do - put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user2) + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", guest) expect(response).to have_http_status(403) end end @@ -250,10 +319,10 @@ describe API::Branches, api: true do end it "update branches with dots in branch name" do - put api("/projects/#{project.id}/repository/branches/with.1.2.3/unprotect", user) + put api("/projects/#{project.id}/repository/branches/#{branch_with_dot.name}/unprotect", user) expect(response).to have_http_status(200) - expect(json_response['name']).to eq("with.1.2.3") + expect(json_response['name']).to eq(branch_with_dot.name) expect(json_response['protected']).to eq(false) end @@ -282,7 +351,7 @@ describe API::Branches, api: true do end it "denies for user without push access" do - post api("/projects/#{project.id}/repository/branches", user2), + post api("/projects/#{project.id}/repository/branches", guest), branch: branch_name, ref: branch_sha expect(response).to have_http_status(403) @@ -325,15 +394,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']).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) + delete api("/projects/#{project.id}/repository/branches/#{branch_with_dot.name}", user) - expect(response).to have_http_status(200) - expect(json_response['branch']).to eq("with.1.2.3") + expect(response).to have_http_status(204) end it 'returns 404 if branch not exists' do @@ -360,13 +428,15 @@ 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 - delete api("/projects/#{project.id}/repository/merged_branches", user2) + delete api("/projects/#{project.id}/repository/merged_branches", guest) expect(response).to have_http_status(403) end end diff --git a/spec/requests/api/broadcast_messages_spec.rb b/spec/requests/api/broadcast_messages_spec.rb index 921d8714173..024fa66848c 100644 --- a/spec/requests/api/broadcast_messages_spec.rb +++ b/spec/requests/api/broadcast_messages_spec.rb @@ -174,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/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb index 81a8856b8f1..d8b3cc041a5 100644 --- a/spec/requests/api/commit_statuses_spec.rb +++ b/spec/requests/api/commit_statuses_spec.rb @@ -151,26 +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', - coverage: 80.0, - target_url: 'http://gitlab.com/status' } - - post api(post_url, developer), optional_params + 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' + } + + 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['coverage']).to eq(80.0) - 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 8b3dfedc5a9..a10d876ffad 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -19,6 +19,7 @@ describe API::Commits, api: true do it "returns project commits" do commit = project.repository.commit + get api("/projects/#{project.id}/repository/commits", user) expect(response).to have_http_status(200) @@ -27,6 +28,16 @@ describe API::Commits, api: true do expect(json_response.first['committer_name']).to eq(commit.committer_name) expect(json_response.first['committer_email']).to eq(commit.committer_email) end + + it 'include correct pagination headers' do + commit_count = project.repository.count_commits(ref: 'master').to_s + + get api("/projects/#{project.id}/repository/commits", user) + + expect(response).to include_pagination_headers + expect(response.headers['X-Total']).to eq(commit_count) + expect(response.headers['X-Page']).to eql('1') + end end context "unauthorized user" do @@ -39,14 +50,26 @@ describe API::Commits, api: true do context "since optional parameter" do it "returns project commits since provided parameter" do commits = project.repository.commits("master") - since = commits.second.created_at + after = commits.second.created_at - get api("/projects/#{project.id}/repository/commits?since=#{since.utc.iso8601}", user) + get api("/projects/#{project.id}/repository/commits?since=#{after.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 + + it 'include correct pagination headers' do + commits = project.repository.commits("master") + after = commits.second.created_at + commit_count = project.repository.count_commits(ref: 'master', after: after).to_s + + get api("/projects/#{project.id}/repository/commits?since=#{after.utc.iso8601}", user) + + expect(response).to include_pagination_headers + expect(response.headers['X-Total']).to eq(commit_count) + expect(response.headers['X-Page']).to eql('1') + end end context "until optional parameter" do @@ -65,6 +88,18 @@ describe API::Commits, api: true do expect(json_response.first["id"]).to eq(commits.second.id) expect(json_response.second["id"]).to eq(commits.third.id) end + + it 'include correct pagination headers' do + commits = project.repository.commits("master") + before = commits.second.created_at + commit_count = project.repository.count_commits(ref: 'master', before: before).to_s + + get api("/projects/#{project.id}/repository/commits?until=#{before.utc.iso8601}", user) + + expect(response).to include_pagination_headers + expect(response.headers['X-Total']).to eq(commit_count) + expect(response.headers['X-Page']).to eql('1') + end end context "invalid xmlschema date parameters" do @@ -79,16 +114,71 @@ describe API::Commits, api: true do context "path optional parameter" do it "returns project commits matching provided path parameter" do path = 'files/ruby/popen.rb' + commit_count = project.repository.count_commits(ref: 'master', path: path).to_s get api("/projects/#{project.id}/repository/commits?path=#{path}", user) expect(json_response.size).to eq(3) expect(json_response.first["id"]).to eq("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") + expect(response).to include_pagination_headers + expect(response.headers['X-Total']).to eq(commit_count) + end + + it 'include correct pagination headers' do + path = 'files/ruby/popen.rb' + commit_count = project.repository.count_commits(ref: 'master', path: path).to_s + + get api("/projects/#{project.id}/repository/commits?path=#{path}", user) + + expect(response).to include_pagination_headers + expect(response.headers['X-Total']).to eq(commit_count) + expect(response.headers['X-Page']).to eql('1') + end + end + + context 'with pagination params' do + let(:page) { 1 } + let(:per_page) { 5 } + let(:ref_name) { 'master' } + let!(:request) do + get api("/projects/#{project.id}/repository/commits?page=#{page}&per_page=#{per_page}&ref_name=#{ref_name}", user) + end + + it 'returns correct headers' do + commit_count = project.repository.count_commits(ref: ref_name).to_s + + expect(response).to include_pagination_headers + expect(response.headers['X-Total']).to eq(commit_count) + expect(response.headers['X-Page']).to eq('1') + expect(response.headers['Link']).to match(/page=1&per_page=5/) + expect(response.headers['Link']).to match(/page=2&per_page=5/) + end + + context 'viewing the first page' do + it 'returns the first 5 commits' do + commit = project.repository.commit + + expect(json_response.size).to eq(per_page) + expect(json_response.first['id']).to eq(commit.id) + expect(response.headers['X-Page']).to eq('1') + end + end + + context 'viewing the third page' do + let(:page) { 3 } + + it 'returns the third 5 commits' do + commit = project.repository.commits('HEAD', offset: (page - 1) * per_page).first + + expect(json_response.size).to eq(per_page) + expect(json_response.first['id']).to eq(commit.id) + expect(response.headers['X-Page']).to eq('3') + end end end end - describe "Create a commit with multiple files and actions" do + describe "POST /projects/:id/repository/commits" do let!(:url) { "/projects/#{project.id}/repository/commits" } it 'returns a 403 unauthorized for user without permissions' do @@ -103,7 +193,7 @@ describe API::Commits, api: true do expect(response).to have_http_status(400) end - context :create do + describe 'create' do let(:message) { 'Created file' } let!(:invalid_c_params) do { @@ -147,8 +237,8 @@ describe API::Commits, api: true do expect(response).to have_http_status(400) end - context 'with project path in URL' do - let(:url) { "/projects/#{project.namespace.path}%2F#{project.path}/repository/commits" } + context 'with project path containing a dot in URL' do + let(:url) { "/projects/#{CGI.escape(project.full_path)}/repository/commits" } it 'a new file in project repo' do post api(url, user), valid_c_params @@ -158,7 +248,7 @@ describe API::Commits, api: true do end end - context :delete do + describe 'delete' do let(:message) { 'Deleted file' } let!(:invalid_d_params) do { @@ -199,7 +289,7 @@ describe API::Commits, api: true do end end - context :move do + describe 'move' do let(:message) { 'Moved file' } let!(:invalid_m_params) do { @@ -244,7 +334,7 @@ describe API::Commits, api: true do end end - context :update do + describe 'update' do let(:message) { 'Updated file' } let!(:invalid_u_params) do { @@ -287,7 +377,7 @@ describe API::Commits, api: true do end end - context "multiple operations" do + describe 'multiple operations' do let(:message) { 'Multiple actions' } let!(:invalid_mo_params) do { diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb index 7e682e91bd1..4f4b18cf0e0 100644 --- a/spec/requests/api/deploy_keys_spec.rb +++ b/spec/requests/api/deploy_keys_spec.rb @@ -116,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 diff --git a/spec/requests/api/doorkeeper_access_spec.rb b/spec/requests/api/doorkeeper_access_spec.rb index bd9ecaf2685..f6fd567eca5 100644 --- a/spec/requests/api/doorkeeper_access_spec.rb +++ b/spec/requests/api/doorkeeper_access_spec.rb @@ -1,17 +1,23 @@ require 'spec_helper' -describe API::API, api: true do +describe API::API, api: true do include ApiHelpers let!(:user) { create(:user) } let!(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) } let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "api" } - describe "when unauthenticated" do + describe "unauthenticated" do it "returns authentication success" do get api("/user"), access_token: token.token expect(response).to have_http_status(200) end + + include_examples 'user login request with unique ip limit' do + def request + get api('/user'), access_token: token.token + end + end end describe "when token invalid" do @@ -26,5 +32,29 @@ describe API::API, api: true do get api("/user", user) expect(response).to have_http_status(200) end + + include_examples 'user login request with unique ip limit' do + def request + get api('/user', user) + end + end + end + + describe "when user is blocked" do + it "returns authentication error" do + user.block + get api("/user"), access_token: token.token + + expect(response).to have_http_status(401) + end + end + + describe "when user is ldap_blocked" do + it "returns authentication error" do + user.ldap_block + get api("/user"), access_token: token.token + + expect(response).to have_http_status(401) + end end end diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb index d0958d39d44..b54ee8e8b85 100644 --- a/spec/requests/api/environments_spec.rb +++ b/spec/requests/api/environments_spec.rb @@ -15,6 +15,8 @@ describe API::Environments, api: true do describe 'GET /projects/:id/environments' do context 'as member of the project' do it 'returns project environments' do + project_data_keys = %w(id http_url_to_repo web_url name name_with_namespace path path_with_namespace) + get api("/projects/#{project.id}/environments", user) expect(response).to have_http_status(200) @@ -23,7 +25,7 @@ describe API::Environments, api: true do 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'].keys).to contain_exactly(*project_data_keys) end end @@ -122,7 +124,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 @@ -141,4 +143,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 a8ce0430401..a7fad7f0bdb 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -5,10 +5,9 @@ describe API::Files, api: true do 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(:file_path) { "files%2Fruby%2Fpopen%2Erb" } let(:params) do { - file_path: file_path, ref: 'master' } end @@ -30,36 +29,54 @@ describe API::Files, api: true do before { project.team << [user, :developer] } - describe "GET /projects/:id/repository/files" do - let(:route) { "/projects/#{project.id}/repository/files" } + def route(file_path = nil) + "/projects/#{project.id}/repository/files/#{file_path}" + end + describe "GET /projects/:id/repository/files/:file_path" do shared_examples_for 'repository files' do - it "returns file info" do - get api(route, current_user), params + it 'returns file attributes as json' do + get api(route(file_path), current_user), params expect(response).to have_http_status(200) - expect(json_response['file_path']).to eq(file_path) + expect(json_response['file_path']).to eq(CGI.unescape(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 'returns file by commit sha' do + # This file is deleted on HEAD + file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee" + params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9" + + get api(route(file_path), current_user), params + + expect(response).to have_http_status(200) + expect(json_response['file_name']).to eq('commit.js.coffee') + expect(Base64.decode64(json_response['content']).lines.first).to eq("class Commit\n") + end + + it 'returns raw file info' do + url = route(file_path) + "/raw" + expect(Gitlab::Workhorse).to receive(:send_git_blob) + + get api(url, current_user), params + + expect(response).to have_http_status(200) + end + + context 'when mandatory params are not given' do it_behaves_like '400 response' do - let(:request) { get api(route, current_user) } + let(:request) { get api(route("any%2Ffile"), current_user) } end end context 'when file_path does not exist' do - let(:params) do - { - file_path: 'app/models/application.rb', - ref: 'master', - } - end + let(:params) { { ref: 'master' } } it_behaves_like '404 response' do - let(:request) { get api(route, current_user), params } + let(:request) { get api(route('app%2Fmodels%2Fapplication%2Erb'), current_user), params } let(:message) { '404 File Not Found' } end end @@ -68,7 +85,7 @@ describe API::Files, api: true do include_context 'disabled repository' it_behaves_like '403 response' do - let(:request) { get api(route, current_user), params } + let(:request) { get api(route(file_path), current_user), params } end end end @@ -82,7 +99,7 @@ describe API::Files, api: true do context 'when unauthenticated', 'and project is private' do it_behaves_like '404 response' do - let(:request) { get api(route), params } + let(:request) { get api(route(file_path)), params } let(:message) { '404 Project Not Found' } end end @@ -95,42 +112,115 @@ describe API::Files, api: true do context 'when authenticated', 'as a guest' do it_behaves_like '403 response' do - let(:request) { get api(route, guest), params } + let(:request) { get api(route(file_path), guest), params } + end + end + end + + describe "GET /projects/:id/repository/files/:file_path/raw" do + shared_examples_for 'repository raw files' do + it 'returns raw file info' do + url = route(file_path) + "/raw" + expect(Gitlab::Workhorse).to receive(:send_git_blob) + + get api(url, current_user), params + + expect(response).to have_http_status(200) + end + + it 'returns file by commit sha' do + # This file is deleted on HEAD + file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee" + params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9" + expect(Gitlab::Workhorse).to receive(:send_git_blob) + + get api(route(file_path) + "/raw", current_user), params + + expect(response).to have_http_status(200) + end + + context 'when mandatory params are not given' do + it_behaves_like '400 response' do + let(:request) { get api(route("any%2Ffile"), current_user) } + end + end + + context 'when file_path does not exist' do + let(:params) { { ref: 'master' } } + + it_behaves_like '404 response' do + let(:request) { get api(route('app%2Fmodels%2Fapplication%2Erb'), 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 api(route(file_path), current_user), params } + end + end + end + + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository raw 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 api(route(file_path)), params } + let(:message) { '404 Project Not Found' } + end + end + + context 'when authenticated', 'as a developer' do + it_behaves_like 'repository raw files' do + let(:current_user) { user } + end + end + + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get api(route(file_path), guest), params } end end end - describe "POST /projects/:id/repository/files" do + describe "POST /projects/:id/repository/files/:file_path" do + let!(:file_path) { "new_subfolder%2Fnewfile%2Erb" } let(:valid_params) do { - file_path: 'newfile.rb', - branch: 'master', - content: 'puts 8', - commit_message: 'Added newfile' + branch: "master", + content: "puts 8", + commit_message: "Added newfile" } end it "creates a new file in project repo" do - post api("/projects/#{project.id}/repository/files", user), valid_params + post api(route(file_path), user), valid_params expect(response).to have_http_status(201) - expect(json_response['file_path']).to eq('newfile.rb') + expect(json_response["file_path"]).to eq(CGI.unescape(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 - post api("/projects/#{project.id}/repository/files", user) + it "returns a 400 bad request if no mandatory params given" do + post api(route("any%2Etxt"), 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(:commit_file). + allow_any_instance_of(Repository).to receive(:create_file). and_return(false) - post api("/projects/#{project.id}/repository/files", user), valid_params + post api(route("any%2Etxt"), user), valid_params expect(response).to have_http_status(400) end @@ -139,7 +229,7 @@ describe API::Files, api: true do it "creates a new file with the specified author" do valid_params.merge!(author_email: author_email, author_name: author_name) - post api("/projects/#{project.id}/repository/files", user), valid_params + post api(route("new_file_with_author%2Etxt"), user), valid_params expect(response).to have_http_status(201) last_commit = project.repository.commit.raw @@ -147,12 +237,25 @@ 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(route("newfile%2Erb"), 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: 'master', content: 'puts 8', commit_message: 'Changed file' @@ -160,17 +263,17 @@ describe API::Files, api: true do end it "updates existing file in project repo" do - put api("/projects/#{project.id}/repository/files", user), valid_params + put api(route(file_path), user), valid_params expect(response).to have_http_status(200) - expect(json_response['file_path']).to eq(file_path) + expect(json_response['file_path']).to eq(CGI.unescape(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 api("/projects/#{project.id}/repository/files", user) + put api(route(file_path), user) expect(response).to have_http_status(400) end @@ -179,7 +282,7 @@ describe API::Files, api: true 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 api("/projects/#{project.id}/repository/files", user), valid_params + put api(route(file_path), user), valid_params expect(response).to have_http_status(200) last_commit = project.repository.commit.raw @@ -192,32 +295,27 @@ describe API::Files, api: true do describe "DELETE /projects/:id/repository/files" do let(:valid_params) do { - file_path: file_path, branch: 'master', commit_message: 'Changed file' } end it "deletes existing file in project repo" do - delete api("/projects/#{project.id}/repository/files", user), valid_params + delete api(route(file_path), 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 - delete api("/projects/#{project.id}/repository/files", user) + delete api(route(file_path), 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(: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 + delete api(route(file_path), user), valid_params expect(response).to have_http_status(400) end @@ -226,21 +324,17 @@ describe API::Files, api: true do it "removes a file with the specified author" do valid_params.merge!(author_email: author_email, author_name: author_name) - delete api("/projects/#{project.id}/repository/files", user), valid_params + delete api(route(file_path), 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 describe "POST /projects/:id/repository/files with binary file" do - let(:file_path) { 'test.bin' } + let(:file_path) { 'test%2Ebin' } let(:put_params) do { - file_path: file_path, branch: 'master', content: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=', commit_message: 'Binary file with a \n should not be touched', @@ -249,21 +343,20 @@ describe API::Files, api: true do end let(:get_params) do { - file_path: file_path, ref: 'master', } end before do - post api("/projects/#{project.id}/repository/files", user), put_params + post api(route(file_path), user), put_params end it "remains unchanged" do - get api("/projects/#{project.id}/repository/files", user), get_params + get api(route(file_path), 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['file_path']).to eq(CGI.unescape(file_path)) + expect(json_response['file_name']).to eq(CGI.unescape(file_path)) expect(json_response['content']).to eq(put_params[:content]) end end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index a59112579e5..2545da7b1db 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -76,6 +76,8 @@ describe API::Groups, api: true do lfs_objects_size: 234, build_artifacts_size: 345, }.stringify_keys + exposed_attributes = attributes.dup + exposed_attributes['job_artifacts_size'] = exposed_attributes.delete('build_artifacts_size') project1.statistics.update!(attributes) @@ -85,7 +87,7 @@ describe API::Groups, api: true do expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response) - .to satisfy_one { |group| group['statistics'] == attributes } + .to satisfy_one { |group| group['statistics'] == exposed_attributes } end end @@ -150,20 +152,10 @@ describe API::Groups, api: true do 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 @@ -186,7 +178,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) @@ -303,9 +295,9 @@ describe API::Groups, api: true do 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 @@ -314,9 +306,9 @@ describe API::Groups, api: true do 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 @@ -398,7 +390,7 @@ describe API::Groups, api: true do expect(response).to have_http_status(200) expect(response).to include_pagination_headers - 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]) end @@ -477,7 +469,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 @@ -506,7 +498,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 @@ -519,7 +511,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/helpers_spec.rb b/spec/requests/api/helpers_spec.rb index a89676fec93..988a57a80ea 100644 --- a/spec/requests/api/helpers_spec.rb +++ b/spec/requests/api/helpers_spec.rb @@ -436,7 +436,7 @@ describe API::Helpers, api: true do context 'current_user is present' do before do - expect_any_instance_of(self.class).to receive(:current_user).and_return(true) + expect_any_instance_of(self.class).to receive(:current_user).at_least(:once).and_return(User.new) end it 'does not raise an error' do diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index ffeacb15f17..63ec00cdf04 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -397,16 +397,53 @@ describe API::Internal, api: true do before do project.team << [user, :developer] - get api("/internal/merge_request_urls?project=#{repo_name}&changes=#{changes}"), secret_token: secret_token end it 'returns link to create new merge request' do + get api("/internal/merge_request_urls?project=#{repo_name}&changes=#{changes}"), secret_token: secret_token + expect(json_response).to match [{ "branch_name" => "new_branch", "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch", "new_merge_request" => true }] end + + it 'returns empty array if printing_merge_request_link_enabled is false' do + project.update!(printing_merge_request_link_enabled: false) + + get api("/internal/merge_request_urls?project=#{repo_name}&changes=#{changes}"), secret_token: secret_token + + expect(json_response).to eq([]) + 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) diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 56ca4c04e7d..52f68fed2cc 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -153,6 +153,16 @@ describe API::Issues, api: true do expect(json_response.first['state']).to eq('opened') end + it 'returns unlabeled issues for "No Label" label' do + get api("/issues", user), labels: 'No Label' + + 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 be_empty + end + it 'returns an empty array if no issue matches labels and state filters' do get api("/issues?labels=#{label.title}&state=closed", user) @@ -212,6 +222,25 @@ describe API::Issues, api: true do 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) @@ -251,6 +280,13 @@ describe API::Issues, api: true do expect(json_response).to be_an Array expect(response_dates).to eq(response_dates.sort) end + + it 'matches V4 response schema' do + get api('/issues', user) + + expect(response).to have_http_status(200) + expect(response).to match_response_schema('public_api/v4/issues') + end end end @@ -377,6 +413,25 @@ describe API::Issues, api: true do 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 + it 'returns an empty array if no group issue matches labels' do get api("#{base_url}?labels=foo,bar", user) @@ -479,6 +534,12 @@ describe API::Issues, api: true do describe "GET /projects/:id/issues" do let(:base_url) { "/projects/#{project.id}" } + it 'returns 404 when project does not exist' do + get api('/projects/1000/issues', non_member) + + expect(response).to have_http_status(404) + end + it "returns 404 on private projects for other users" do private_project = create(:empty_project, :private) create(:issue, project: private_project) @@ -586,6 +647,25 @@ describe API::Issues, api: true do 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) @@ -693,9 +773,9 @@ describe API::Issues, api: true do end end - describe "GET /projects/:id/issues/:issue_id" do + describe "GET /projects/:id/issues/:issue_iid" do it 'exposes known attributes' do - get api("/projects/#{project.id}/issues/#{issue.id}", user) + get api("/projects/#{project.id}/issues/#{issue.iid}", user) expect(response).to have_http_status(200) expect(json_response['id']).to eq(issue.id) @@ -713,8 +793,8 @@ describe API::Issues, api: true do expect(json_response['confidential']).to be_falsy end - it "returns a project issue by id" do - get api("/projects/#{project.id}/issues/#{issue.id}", user) + it "returns a project issue by internal id" do + get api("/projects/#{project.id}/issues/#{issue.iid}", user) expect(response).to have_http_status(200) expect(json_response['title']).to eq(issue.title) @@ -726,40 +806,52 @@ describe API::Issues, api: true do expect(response).to have_http_status(404) end + it "returns 404 if the issue ID is used" do + get api("/projects/#{project.id}/issues/#{issue.id}", user) + + expect(response).to have_http_status(404) + end + context 'confidential issues' do it "returns 404 for non project members" do - get api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member) + get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", non_member) + expect(response).to have_http_status(404) end it "returns 404 for project members with guest role" do - get api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest) + get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", guest) + expect(response).to have_http_status(404) end it "returns confidential issue for project members" do - get api("/projects/#{project.id}/issues/#{confidential_issue.id}", user) + get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user) + expect(response).to have_http_status(200) expect(json_response['title']).to eq(confidential_issue.title) expect(json_response['iid']).to eq(confidential_issue.iid) end it "returns confidential issue for author" do - get api("/projects/#{project.id}/issues/#{confidential_issue.id}", author) + get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", author) + expect(response).to have_http_status(200) expect(json_response['title']).to eq(confidential_issue.title) expect(json_response['iid']).to eq(confidential_issue.iid) end it "returns confidential issue for assignee" do - get api("/projects/#{project.id}/issues/#{confidential_issue.id}", assignee) + get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", assignee) + expect(response).to have_http_status(200) expect(json_response['title']).to eq(confidential_issue.title) expect(json_response['iid']).to eq(confidential_issue.iid) end it "returns confidential issue for admin" do - get api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin) + get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", admin) + expect(response).to have_http_status(200) expect(json_response['title']).to eq(confidential_issue.title) expect(json_response['iid']).to eq(confidential_issue.iid) @@ -775,7 +867,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 @@ -852,29 +944,34 @@ describe API::Issues, api: true do ]) end - context 'resolving issues in a merge request' do + context 'resolving discussions' do let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first } let(:merge_request) { discussion.noteable } let(:project) { merge_request.source_project } + before do project.team << [user, :master] - post api("/projects/#{project.id}/issues", user), - title: 'New Issue', - merge_request_for_resolving_discussions: merge_request.iid end - it 'creates a new project issue' do - expect(response).to have_http_status(:created) - end + context 'resolving all discussions in a merge request' do + before do + post api("/projects/#{project.id}/issues", user), + title: 'New Issue', + merge_request_to_resolve_discussions_of: merge_request.iid + end - it 'resolves the discussions in a merge request' do - discussion.first_note.reload - - expect(discussion.resolved?).to be(true) + it_behaves_like 'creating an issue resolving discussions through the API' end - it 'assigns a description to the issue mentioning the merge request' do - expect(json_response['description']).to include(merge_request.to_reference) + context 'resolving a single discussion' do + before do + post api("/projects/#{project.id}/issues", user), + title: 'New Issue', + merge_request_to_resolve_discussions_of: merge_request.iid, + discussion_to_resolve: discussion.id + end + + it_behaves_like 'creating an issue resolving discussions through the API' end end @@ -940,23 +1037,29 @@ describe API::Issues, api: true do end end - describe "PUT /projects/:id/issues/:issue_id to update only title" do + describe "PUT /projects/:id/issues/:issue_iid to update only title" do it "updates a project issue" do - put api("/projects/#{project.id}/issues/#{issue.id}", user), + put api("/projects/#{project.id}/issues/#{issue.iid}", user), title: 'updated title' expect(response).to have_http_status(200) expect(json_response['title']).to eq('updated title') end - it "returns 404 error if issue id not found" do + it "returns 404 error if issue iid not found" do put api("/projects/#{project.id}/issues/44444", user), title: 'updated title' expect(response).to have_http_status(404) end - it 'allows special label names' do + it "returns 404 error if issue id is used instead of the iid" do put api("/projects/#{project.id}/issues/#{issue.id}", user), + title: 'updated title' + expect(response).to have_http_status(404) + end + + it 'allows special label names' do + put api("/projects/#{project.id}/issues/#{issue.iid}", user), title: 'updated title', labels: 'label, label?, label&foo, ?, &' @@ -970,40 +1073,40 @@ describe API::Issues, api: true do context 'confidential issues' do it "returns 403 for non project members" do - put api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member), + put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", non_member), title: 'updated title' expect(response).to have_http_status(403) end it "returns 403 for project members with guest role" do - put api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest), + put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", guest), title: 'updated title' expect(response).to have_http_status(403) end it "updates a confidential issue for project members" do - put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user), + put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user), title: 'updated title' expect(response).to have_http_status(200) expect(json_response['title']).to eq('updated title') end it "updates a confidential issue for author" do - put api("/projects/#{project.id}/issues/#{confidential_issue.id}", author), + put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", author), title: 'updated title' expect(response).to have_http_status(200) expect(json_response['title']).to eq('updated title') end it "updates a confidential issue for admin" do - put api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin), + put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", admin), title: 'updated title' expect(response).to have_http_status(200) expect(json_response['title']).to eq('updated title') end it 'sets an issue to confidential' do - put api("/projects/#{project.id}/issues/#{issue.id}", user), + put api("/projects/#{project.id}/issues/#{issue.iid}", user), confidential: true expect(response).to have_http_status(200) @@ -1011,7 +1114,7 @@ describe API::Issues, api: true do end it 'makes a confidential issue public' do - put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user), + put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user), confidential: false expect(response).to have_http_status(200) @@ -1019,7 +1122,7 @@ describe API::Issues, api: true do end it 'does not update a confidential issue with wrong confidential flag' do - put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user), + put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user), confidential: 'foo' expect(response).to have_http_status(400) @@ -1028,7 +1131,7 @@ describe API::Issues, api: true do end end - describe 'PUT /projects/:id/issues/:issue_id with spam filtering' do + describe 'PUT /projects/:id/issues/:issue_iid with spam filtering' do let(:params) do { title: 'updated title', @@ -1041,7 +1144,7 @@ describe API::Issues, api: true 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 + put api("/projects/#{project.id}/issues/#{issue.iid}", user), params expect(response).to have_http_status(400) expect(json_response['message']).to eq({ "error" => "Spam detected" }) @@ -1055,12 +1158,12 @@ describe API::Issues, api: true do end end - describe 'PUT /projects/:id/issues/:issue_id to update labels' do + describe 'PUT /projects/:id/issues/:issue_iid to update labels' do let!(:label) { create(:label, title: 'dummy', project: project) } let!(:label_link) { create(:label_link, label: label, target: issue) } it 'does not update labels if not present' do - put api("/projects/#{project.id}/issues/#{issue.id}", user), + put api("/projects/#{project.id}/issues/#{issue.iid}", user), title: 'updated title' expect(response).to have_http_status(200) expect(json_response['labels']).to eq([label.title]) @@ -1071,7 +1174,7 @@ describe API::Issues, api: true do label.toggle_subscription(user2, project) perform_enqueued_jobs do - put api("/projects/#{project.id}/issues/#{issue.id}", user), + put api("/projects/#{project.id}/issues/#{issue.iid}", user), title: 'updated title', labels: label.title end @@ -1079,14 +1182,14 @@ describe API::Issues, api: true do end it 'removes all labels' do - put api("/projects/#{project.id}/issues/#{issue.id}", user), labels: '' + put api("/projects/#{project.id}/issues/#{issue.iid}", user), labels: '' expect(response).to have_http_status(200) expect(json_response['labels']).to eq([]) end it 'updates labels' do - put api("/projects/#{project.id}/issues/#{issue.id}", user), + put api("/projects/#{project.id}/issues/#{issue.iid}", user), labels: 'foo,bar' expect(response).to have_http_status(200) expect(json_response['labels']).to include 'foo' @@ -1094,7 +1197,7 @@ describe API::Issues, api: true do end it 'allows special label names' do - put api("/projects/#{project.id}/issues/#{issue.id}", user), + put api("/projects/#{project.id}/issues/#{issue.iid}", user), labels: 'label:foo, label-bar,label_bar,label/bar,label?bar,label&bar,?,&' expect(response.status).to eq(200) expect(json_response['labels']).to include 'label:foo' @@ -1108,7 +1211,7 @@ describe API::Issues, api: true do end it 'returns 400 if title is too long' do - put api("/projects/#{project.id}/issues/#{issue.id}", user), + put api("/projects/#{project.id}/issues/#{issue.iid}", user), title: 'g' * 256 expect(response).to have_http_status(400) expect(json_response['message']['title']).to eq([ @@ -1117,9 +1220,9 @@ describe API::Issues, api: true do end end - describe "PUT /projects/:id/issues/:issue_id to update state and label" do + describe "PUT /projects/:id/issues/:issue_iid to update state and label" do it "updates a project issue" do - put api("/projects/#{project.id}/issues/#{issue.id}", user), + put api("/projects/#{project.id}/issues/#{issue.iid}", user), labels: 'label2', state_event: "close" expect(response).to have_http_status(200) @@ -1128,7 +1231,7 @@ describe API::Issues, api: true do end it 'reopens a project isssue' do - put api("/projects/#{project.id}/issues/#{closed_issue.id}", user), state_event: 'reopen' + put api("/projects/#{project.id}/issues/#{closed_issue.iid}", user), state_event: 'reopen' expect(response).to have_http_status(200) expect(json_response['state']).to eq 'reopened' @@ -1137,7 +1240,7 @@ describe API::Issues, api: true do context 'when an admin or owner makes the request' do it 'accepts the update date to be set' do update_time = 2.weeks.ago - put api("/projects/#{project.id}/issues/#{issue.id}", user), + put api("/projects/#{project.id}/issues/#{issue.iid}", user), labels: 'label3', state_event: 'close', updated_at: update_time expect(response).to have_http_status(200) @@ -1147,25 +1250,25 @@ describe API::Issues, api: true do end end - describe 'PUT /projects/:id/issues/:issue_id to update due date' do + describe 'PUT /projects/:id/issues/:issue_iid to update due date' do it 'creates a new project issue' do due_date = 2.weeks.from_now.strftime('%Y-%m-%d') - put api("/projects/#{project.id}/issues/#{issue.id}", user), due_date: due_date + put api("/projects/#{project.id}/issues/#{issue.iid}", user), due_date: due_date expect(response).to have_http_status(200) expect(json_response['due_date']).to eq(due_date) end end - describe "DELETE /projects/:id/issues/:issue_id" do + describe "DELETE /projects/:id/issues/:issue_iid" do it "rejects a non member from deleting an issue" do - delete api("/projects/#{project.id}/issues/#{issue.id}", non_member) + delete api("/projects/#{project.id}/issues/#{issue.iid}", non_member) expect(response).to have_http_status(403) end it "rejects a developer from deleting an issue" do - delete api("/projects/#{project.id}/issues/#{issue.id}", author) + delete api("/projects/#{project.id}/issues/#{issue.iid}", author) expect(response).to have_http_status(403) end @@ -1174,9 +1277,9 @@ describe API::Issues, api: true do let(:project) { create(:empty_project, namespace: owner.namespace) } 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' + delete api("/projects/#{project.id}/issues/#{issue.iid}", owner) + + expect(response).to have_http_status(204) end end @@ -1187,14 +1290,20 @@ describe API::Issues, api: true do expect(response).to have_http_status(404) end end + + it 'returns 404 when using the issue ID instead of IID' do + delete api("/projects/#{project.id}/issues/#{issue.id}", user) + + expect(response).to have_http_status(404) + end end - describe '/projects/:id/issues/:issue_id/move' do + describe '/projects/:id/issues/:issue_iid/move' do let!(:target_project) { create(:empty_project, path: 'project2', creator_id: user.id, namespace: user.namespace ) } let!(:target_project2) { create(:empty_project, creator_id: non_member.id, namespace: non_member.namespace ) } it 'moves an issue' do - post api("/projects/#{project.id}/issues/#{issue.id}/move", user), + post api("/projects/#{project.id}/issues/#{issue.iid}/move", user), to_project_id: target_project.id expect(response).to have_http_status(201) @@ -1203,7 +1312,7 @@ describe API::Issues, api: true do context 'when source and target projects are the same' do it 'returns 400 when trying to move an issue' do - post api("/projects/#{project.id}/issues/#{issue.id}/move", user), + post api("/projects/#{project.id}/issues/#{issue.iid}/move", user), to_project_id: project.id expect(response).to have_http_status(400) @@ -1213,7 +1322,7 @@ describe API::Issues, api: true do context 'when the user does not have the permission to move issues' do it 'returns 400 when trying to move an issue' do - post api("/projects/#{project.id}/issues/#{issue.id}/move", user), + post api("/projects/#{project.id}/issues/#{issue.iid}/move", user), to_project_id: target_project2.id expect(response).to have_http_status(400) @@ -1222,13 +1331,23 @@ describe API::Issues, api: true do end it 'moves the issue to another namespace if I am admin' do - post api("/projects/#{project.id}/issues/#{issue.id}/move", admin), + post api("/projects/#{project.id}/issues/#{issue.iid}/move", admin), to_project_id: target_project2.id expect(response).to have_http_status(201) expect(json_response['project_id']).to eq(target_project2.id) end + context 'when using the issue ID instead of iid' do + it 'returns 404 when trying to move an issue' do + post api("/projects/#{project.id}/issues/#{issue.id}/move", user), + to_project_id: target_project.id + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Issue Not Found') + end + end + context 'when issue does not exist' do it 'returns 404 when trying to move an issue' do post api("/projects/#{project.id}/issues/123/move", user), @@ -1241,7 +1360,7 @@ describe API::Issues, api: true do context 'when source project does not exist' do it 'returns 404 when trying to move an issue' do - post api("/projects/123/issues/#{issue.id}/move", user), + post api("/projects/123/issues/#{issue.iid}/move", user), to_project_id: target_project.id expect(response).to have_http_status(404) @@ -1251,7 +1370,7 @@ describe API::Issues, api: true do context 'when target project does not exist' do it 'returns 404 when trying to move an issue' do - post api("/projects/#{project.id}/issues/#{issue.id}/move", user), + post api("/projects/#{project.id}/issues/#{issue.iid}/move", user), to_project_id: 123 expect(response).to have_http_status(404) @@ -1259,16 +1378,16 @@ describe API::Issues, api: true do end end - describe 'POST :id/issues/:issue_id/subscribe' do + describe 'POST :id/issues/:issue_iid/subscribe' do it 'subscribes to an issue' do - post api("/projects/#{project.id}/issues/#{issue.id}/subscribe", user2) + post api("/projects/#{project.id}/issues/#{issue.iid}/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}/subscribe", user) + post api("/projects/#{project.id}/issues/#{issue.iid}/subscribe", user) expect(response).to have_http_status(304) end @@ -1279,8 +1398,14 @@ describe API::Issues, api: true do expect(response).to have_http_status(404) end + it 'returns 404 if the issue ID is used instead of the iid' do + post api("/projects/#{project.id}/issues/#{issue.id}/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}/subscribe", non_member) + post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/subscribe", non_member) expect(response).to have_http_status(404) end @@ -1288,14 +1413,14 @@ describe API::Issues, api: true do describe 'POST :id/issues/:issue_id/unsubscribe' do it 'unsubscribes from an issue' do - post api("/projects/#{project.id}/issues/#{issue.id}/unsubscribe", user) + post api("/projects/#{project.id}/issues/#{issue.iid}/unsubscribe", user) expect(response).to have_http_status(201) expect(json_response['subscribed']).to eq(false) end it 'returns 304 if not subscribed' do - post api("/projects/#{project.id}/issues/#{issue.id}/unsubscribe", user2) + post api("/projects/#{project.id}/issues/#{issue.iid}/unsubscribe", user2) expect(response).to have_http_status(304) end @@ -1306,8 +1431,14 @@ describe API::Issues, api: true do expect(response).to have_http_status(404) end + it 'returns 404 if using the issue ID instead of iid' do + post api("/projects/#{project.id}/issues/#{issue.id}/unsubscribe", 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}/unsubscribe", non_member) + post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/unsubscribe", non_member) expect(response).to have_http_status(404) end diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb new file mode 100644 index 00000000000..9450701064b --- /dev/null +++ b/spec/requests/api/jobs_spec.rb @@ -0,0 +1,480 @@ +require 'spec_helper' + +describe API::Jobs, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:api_user) { user } + let!(:project) { create(:project, :repository, creator: user, public_builds: false) } + let!(:developer) { create(:project_member, :developer, user: user, project: project) } + let(:reporter) { create(:project_member, :reporter, project: project) } + let(:guest) { create(:project_member, :guest, project: project) } + let!(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) } + let!(:build) { create(:ci_build, pipeline: pipeline) } + + describe 'GET /projects/:id/jobs' do + let(:query) { Hash.new } + + before do + get api("/projects/#{project.id}/jobs", api_user), query + end + + context 'authorized user' do + it 'returns project jobs' do + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + end + + it 'returns correct values' do + expect(json_response).not_to be_empty + expect(json_response.first['commit']['id']).to eq project.commit.id + end + + it 'returns pipeline data' do + json_build = json_response.first + + expect(json_build['pipeline']).not_to be_empty + expect(json_build['pipeline']['id']).to eq build.pipeline.id + expect(json_build['pipeline']['ref']).to eq build.pipeline.ref + expect(json_build['pipeline']['sha']).to eq build.pipeline.sha + expect(json_build['pipeline']['status']).to eq build.pipeline.status + end + + context 'filter project with one scope element' do + let(:query) { { 'scope' => 'pending' } } + + it do + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + end + end + + context 'filter project with array of scope elements' do + let(:query) { { scope: %w(pending running) } } + + it do + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + end + end + + context 'respond 400 when scope contains invalid state' do + let(:query) { { scope: %w(unknown running) } } + + it { expect(response).to have_http_status(400) } + end + end + + context 'unauthorized user' do + let(:api_user) { nil } + + it 'does not return project builds' do + expect(response).to have_http_status(401) + end + end + end + + describe 'GET /projects/:id/pipelines/:pipeline_id/jobs' do + let(:query) { Hash.new } + + before do + get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), query + end + + context 'authorized user' do + it 'returns pipeline jobs' do + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + end + + it 'returns correct values' do + expect(json_response).not_to be_empty + expect(json_response.first['commit']['id']).to eq project.commit.id + end + + it 'returns pipeline data' do + json_build = json_response.first + + expect(json_build['pipeline']).not_to be_empty + expect(json_build['pipeline']['id']).to eq build.pipeline.id + expect(json_build['pipeline']['ref']).to eq build.pipeline.ref + expect(json_build['pipeline']['sha']).to eq build.pipeline.sha + expect(json_build['pipeline']['status']).to eq build.pipeline.status + end + + context 'filter jobs with one scope element' do + let(:query) { { 'scope' => 'pending' } } + + it do + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + end + end + + context 'filter jobs with array of scope elements' do + let(:query) { { scope: %w(pending running) } } + + it do + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + end + end + + context 'respond 400 when scope contains invalid state' do + let(:query) { { scope: %w(unknown running) } } + + it { expect(response).to have_http_status(400) } + end + + context 'jobs in different pipelines' do + let!(:pipeline2) { create(:ci_empty_pipeline, project: project) } + let!(:build2) { create(:ci_build, pipeline: pipeline2) } + + it 'excludes jobs from other pipelines' do + json_response.each { |job| expect(job['pipeline']['id']).to eq(pipeline.id) } + end + end + end + + context 'unauthorized user' do + let(:api_user) { nil } + + it 'does not return jobs' do + expect(response).to have_http_status(401) + end + end + end + + describe 'GET /projects/:id/jobs/:job_id' do + before do + get api("/projects/#{project.id}/jobs/#{build.id}", api_user) + end + + context 'authorized user' do + it 'returns specific job data' do + expect(response).to have_http_status(200) + expect(json_response['name']).to eq('test') + end + + it 'returns pipeline data' do + json_build = json_response + expect(json_build['pipeline']).not_to be_empty + expect(json_build['pipeline']['id']).to eq build.pipeline.id + expect(json_build['pipeline']['ref']).to eq build.pipeline.ref + expect(json_build['pipeline']['sha']).to eq build.pipeline.sha + expect(json_build['pipeline']['status']).to eq build.pipeline.status + end + end + + context 'unauthorized user' do + let(:api_user) { nil } + + it 'does not return specific job data' do + expect(response).to have_http_status(401) + end + end + end + + describe 'GET /projects/:id/jobs/:job_id/artifacts' do + before do + get api("/projects/#{project.id}/jobs/#{build.id}/artifacts", api_user) + end + + context 'job with artifacts' do + let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } + + context 'authorized user' do + let(:download_headers) do + { 'Content-Transfer-Encoding' => 'binary', + 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' } + end + + it 'returns specific job artifacts' do + expect(response).to have_http_status(200) + expect(response.headers).to include(download_headers) + expect(response.body).to match_file(build.artifacts_file.file.file) + end + end + + context 'unauthorized user' do + let(:api_user) { nil } + + it 'does not return specific job artifacts' do + expect(response).to have_http_status(401) + end + end + end + + it 'does not return job artifacts if not uploaded' do + expect(response).to have_http_status(404) + end + end + + describe 'GET /projects/:id/artifacts/:ref_name/download?job=name' do + let(:api_user) { reporter.user } + let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } + + before do + build.success + end + + def get_for_ref(ref = pipeline.ref, job = build.name) + get api("/projects/#{project.id}/jobs/artifacts/#{ref}/download", api_user), job: job + end + + context 'when not logged in' do + let(:api_user) { nil } + + before do + get_for_ref + end + + it 'gives 401' do + expect(response).to have_http_status(401) + end + end + + context 'when logging as guest' do + let(:api_user) { guest.user } + + before do + get_for_ref + end + + it 'gives 403' do + expect(response).to have_http_status(403) + end + end + + context 'non-existing job' do + shared_examples 'not found' do + it { expect(response).to have_http_status(:not_found) } + end + + context 'has no such ref' do + before do + get_for_ref('TAIL') + end + + it_behaves_like 'not found' + end + + context 'has no such job' do + before do + get_for_ref(pipeline.ref, 'NOBUILD') + end + + it_behaves_like 'not found' + end + end + + context 'find proper job' do + shared_examples 'a valid file' do + let(:download_headers) do + { 'Content-Transfer-Encoding' => 'binary', + 'Content-Disposition' => + "attachment; filename=#{build.artifacts_file.filename}" } + end + + it { expect(response).to have_http_status(200) } + it { expect(response.headers).to include(download_headers) } + end + + context 'with regular branch' do + before do + pipeline.reload + pipeline.update(ref: 'master', + sha: project.commit('master').sha) + + get_for_ref('master') + end + + it_behaves_like 'a valid file' + end + + context 'with branch name containing slash' do + before do + pipeline.reload + pipeline.update(ref: 'improve/awesome', + sha: project.commit('improve/awesome').sha) + end + + before do + get_for_ref('improve/awesome') + end + + it_behaves_like 'a valid file' + end + end + end + + describe 'GET /projects/:id/jobs/:job_id/trace' do + let(:build) { create(:ci_build, :trace, pipeline: pipeline) } + + before do + get api("/projects/#{project.id}/jobs/#{build.id}/trace", api_user) + end + + context 'authorized user' do + it 'returns specific job trace' do + expect(response).to have_http_status(200) + expect(response.body).to eq(build.trace) + end + end + + context 'unauthorized user' do + let(:api_user) { nil } + + it 'does not return specific job trace' do + expect(response).to have_http_status(401) + end + end + end + + describe 'POST /projects/:id/jobs/:job_id/cancel' do + before do + post api("/projects/#{project.id}/jobs/#{build.id}/cancel", api_user) + end + + context 'authorized user' do + context 'user with :update_build persmission' do + it 'cancels running or pending job' do + expect(response).to have_http_status(201) + expect(project.builds.first.status).to eq('canceled') + end + end + + context 'user without :update_build permission' do + let(:api_user) { reporter.user } + + it 'does not cancel job' do + expect(response).to have_http_status(403) + end + end + end + + context 'unauthorized user' do + let(:api_user) { nil } + + it 'does not cancel job' do + expect(response).to have_http_status(401) + end + end + end + + describe 'POST /projects/:id/jobs/:job_id/retry' do + let(:build) { create(:ci_build, :canceled, pipeline: pipeline) } + + before do + post api("/projects/#{project.id}/jobs/#{build.id}/retry", api_user) + end + + context 'authorized user' do + context 'user with :update_build permission' do + it 'retries non-running job' do + expect(response).to have_http_status(201) + expect(project.builds.first.status).to eq('canceled') + expect(json_response['status']).to eq('pending') + end + end + + context 'user without :update_build permission' do + let(:api_user) { reporter.user } + + it 'does not retry job' do + expect(response).to have_http_status(403) + end + end + end + + context 'unauthorized user' do + let(:api_user) { nil } + + it 'does not retry job' do + expect(response).to have_http_status(401) + end + end + end + + describe 'POST /projects/:id/jobs/:job_id/erase' do + before do + post api("/projects/#{project.id}/jobs/#{build.id}/erase", user) + end + + context 'job is erasable' do + let(:build) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline) } + + it 'erases job content' do + expect(response).to have_http_status(201) + expect(build.trace).to be_empty + expect(build.artifacts_file.exists?).to be_falsy + expect(build.artifacts_metadata.exists?).to be_falsy + end + + it 'updates job' do + build.reload + expect(build.erased_at).to be_truthy + expect(build.erased_by).to eq(user) + end + end + + context 'job is not erasable' do + let(:build) { create(:ci_build, :trace, project: project, pipeline: pipeline) } + + it 'responds with forbidden' do + expect(response).to have_http_status(403) + end + end + end + + describe 'POST /projects/:id/jobs/:build_id/artifacts/keep' do + before do + post api("/projects/#{project.id}/jobs/#{build.id}/artifacts/keep", user) + end + + context 'artifacts did not expire' do + let(:build) do + create(:ci_build, :trace, :artifacts, :success, + project: project, pipeline: pipeline, artifacts_expire_at: Time.now + 7.days) + end + + it 'keeps artifacts' do + expect(response).to have_http_status(200) + expect(build.reload.artifacts_expire_at).to be_nil + end + end + + context 'no artifacts' do + let(:build) { create(:ci_build, project: project, pipeline: pipeline) } + + it 'responds with not found' do + expect(response).to have_http_status(404) + end + end + end + + describe 'POST /projects/:id/jobs/:job_id/play' do + before do + post api("/projects/#{project.id}/jobs/#{build.id}/play", user) + end + + context 'on an playable job' do + let(:build) { create(:ci_build, :manual, project: project, pipeline: pipeline) } + + it 'plays the job' do + expect(response).to have_http_status(200) + expect(json_response['user']['id']).to eq(user.id) + expect(json_response['id']).to eq(build.id) + end + end + + context 'on a non-playable job' do + it 'returns a status code 400, Bad Request' do + expect(response).to have_http_status 400 + expect(response.body).to match("Unplayable Job") + end + end + end +end diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb index 566d11bba57..a1adaba7b98 100644 --- a/spec/requests/api/labels_spec.rb +++ b/spec/requests/api/labels_spec.rb @@ -21,11 +21,11 @@ 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) @@ -175,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 diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb index 31166b50033..2d37d026a39 100644 --- a/spec/requests/api/members_spec.rb +++ b/spec/requests/api/members_spec.rb @@ -173,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 @@ -230,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 @@ -263,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 @@ -283,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 @@ -342,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 1d02e827183..79f3151ba52 100644 --- a/spec/requests/api/merge_request_diffs_spec.rb +++ b/spec/requests/api/merge_request_diffs_spec.rb @@ -13,9 +13,9 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs', api: true do project.team << [user, :master] end - describe 'GET /projects/:id/merge_requests/:merge_request_id/versions' do + describe 'GET /projects/:id/merge_requests/:merge_request_iid/versions' do it 'returns 200 for a valid merge request' do - get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions", user) + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/versions", user) merge_request_diff = merge_request.merge_request_diffs.first expect(response.status).to eq 200 @@ -26,16 +26,22 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs', api: true do expect(json_response.first['head_commit_sha']).to eq(merge_request_diff.head_commit_sha) end - it 'returns a 404 when merge_request_id not found' do + it 'returns a 404 when merge_request id is used instead of the iid' do + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions", user) + expect(response).to have_http_status(404) + end + + it 'returns a 404 when merge_request_iid not found' do get api("/projects/#{project.id}/merge_requests/999/versions", user) expect(response).to have_http_status(404) end end - describe 'GET /projects/:id/merge_requests/:merge_request_id/versions/:version_id' do + describe 'GET /projects/:id/merge_requests/:merge_request_iid/versions/:version_id' do + let(:merge_request_diff) { merge_request.merge_request_diffs.first } + it 'returns a 200 for a valid merge request' do - merge_request_diff = merge_request.merge_request_diffs.first - get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/#{merge_request_diff.id}", user) + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/versions/#{merge_request_diff.id}", user) expect(response.status).to eq 200 expect(json_response['id']).to eq(merge_request_diff.id) @@ -43,8 +49,18 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs', api: true do expect(json_response['diffs'].size).to eq(merge_request_diff.diffs.size) end - it 'returns a 404 when merge_request_id not found' do - get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/999", user) + it 'returns a 404 when merge_request id is used instead of the iid' do + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/#{merge_request_diff.id}", user) + expect(response).to have_http_status(404) + end + + it 'returns a 404 when merge_request version_id is not found' do + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/versions/999", user) + expect(response).to have_http_status(404) + end + + it 'returns a 404 when merge_request_iid is not found' do + get api("/projects/#{project.id}/merge_requests/12345/versions/#{merge_request_diff.id}", user) expect(response).to have_http_status(404) end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index c125df8b90b..9aba1d75612 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -93,6 +93,13 @@ describe API::MergeRequests, api: true do expect(json_response.first['id']).to eq merge_request_closed.id end + it 'matches V4 response schema' do + get api("/projects/#{project.id}/merge_requests", user) + + expect(response).to have_http_status(200) + expect(response).to match_response_schema('public_api/v4/merge_requests') + end + context "with ordering" do before do @mr_later = mr_with_later_created_and_updated_at_time @@ -146,9 +153,9 @@ describe API::MergeRequests, api: true do end end - describe "GET /projects/:id/merge_requests/:merge_request_id" do + describe "GET /projects/:id/merge_requests/:merge_request_iid" do it 'exposes known attributes' do - get api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user) + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user) expect(response).to have_http_status(200) expect(json_response['id']).to eq(merge_request.id) @@ -170,14 +177,14 @@ 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 end it "returns merge_request" do - get api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user) + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user) expect(response).to have_http_status(200) expect(json_response['title']).to eq(merge_request.title) expect(json_response['iid']).to eq(merge_request.iid) @@ -187,25 +194,31 @@ describe API::MergeRequests, api: true do expect(json_response['force_close_merge_request']).to be_falsy end - it "returns a 404 error if merge_request_id not found" do + it "returns a 404 error if merge_request_iid not found" do get api("/projects/#{project.id}/merge_requests/999", user) expect(response).to have_http_status(404) end + it "returns a 404 error if merge_request `id` is used instead of iid" do + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user) + + expect(response).to have_http_status(404) + end + context 'Work in Progress' do let!(:merge_request_wip) { create(:merge_request, author: user, assignee: user, source_project: project, target_project: project, title: "WIP: Test", created_at: base_time + 1.second) } it "returns merge_request" do - get api("/projects/#{project.id}/merge_requests/#{merge_request_wip.id}", user) + get api("/projects/#{project.id}/merge_requests/#{merge_request_wip.iid}", user) expect(response).to have_http_status(200) expect(json_response['work_in_progress']).to eq(true) end end end - describe 'GET /projects/:id/merge_requests/:merge_request_id/commits' do + describe 'GET /projects/:id/merge_requests/:merge_request_iid/commits' do it 'returns a 200 when merge request is valid' do - get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/commits", user) + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/commits", user) commit = merge_request.commits.first expect(response.status).to eq 200 @@ -216,24 +229,36 @@ describe API::MergeRequests, api: true do expect(json_response.first['title']).to eq(commit.title) end - it 'returns a 404 when merge_request_id not found' do + it 'returns a 404 when merge_request_iid not found' do get api("/projects/#{project.id}/merge_requests/999/commits", user) expect(response).to have_http_status(404) end + + it 'returns a 404 when merge_request id is used instead of iid' do + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/commits", user) + + expect(response).to have_http_status(404) + end end - describe 'GET /projects/:id/merge_requests/:merge_request_id/changes' do + describe 'GET /projects/:id/merge_requests/:merge_request_iid/changes' do it 'returns the change information of the merge_request' do - get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/changes", user) + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/changes", user) expect(response.status).to eq 200 expect(json_response['changes'].size).to eq(merge_request.diffs.size) end - it 'returns a 404 when merge_request_id not found' do + it 'returns a 404 when merge_request_iid not found' do get api("/projects/#{project.id}/merge_requests/999/changes", user) expect(response).to have_http_status(404) end + + it 'returns a 404 when merge_request id is used instead of iid' do + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/changes", user) + + expect(response).to have_http_status(404) + end end describe "POST /projects/:id/merge_requests" do @@ -250,7 +275,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 @@ -393,7 +418,7 @@ describe API::MergeRequests, api: true do end end - describe "DELETE /projects/:id/merge_requests/:merge_request_id" do + describe "DELETE /projects/:id/merge_requests/:merge_request_iid" do context "when the user is developer" do let(:developer) { create(:user) } @@ -402,25 +427,37 @@ describe API::MergeRequests, api: true do end it "denies the deletion of the merge request" do - delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}", developer) + delete api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", developer) expect(response).to have_http_status(403) end end context "when the user is project owner" do it "destroys the merge request owners can destroy" do + delete api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user) + + expect(response).to have_http_status(204) + end + + it "returns 404 for an invalid merge request IID" do + delete api("/projects/#{project.id}/merge_requests/12345", user) + + expect(response).to have_http_status(404) + end + + it "returns 404 if the merge request id is used instead of iid" 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(404) end end end - describe "PUT /projects/:id/merge_requests/:merge_request_id/merge" do + describe "PUT /projects/:id/merge_requests/:merge_request_iid/merge" do let(:pipeline) { create(:ci_pipeline_without_jobs) } it "returns merge_request in case of success" do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user) expect(response).to have_http_status(200) end @@ -429,7 +466,7 @@ describe API::MergeRequests, api: true do allow_any_instance_of(MergeRequest). to receive(:can_be_merged?).and_return(false) - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user) expect(response).to have_http_status(406) expect(json_response['message']).to eq('Branch cannot be merged') @@ -437,14 +474,14 @@ describe API::MergeRequests, api: true do it "returns 405 if merge_request is not open" do merge_request.close - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user) expect(response).to have_http_status(405) expect(json_response['message']).to eq('405 Method Not Allowed') end it "returns 405 if merge_request is a work in progress" do merge_request.update_attribute(:title, "WIP: #{merge_request.title}") - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user) expect(response).to have_http_status(405) expect(json_response['message']).to eq('405 Method Not Allowed') end @@ -452,7 +489,7 @@ describe API::MergeRequests, api: true do it 'returns 405 if the build failed for a merge request that requires success' do allow_any_instance_of(MergeRequest).to receive(:mergeable_ci_state?).and_return(false) - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user) expect(response).to have_http_status(405) expect(json_response['message']).to eq('405 Method Not Allowed') @@ -461,20 +498,20 @@ describe API::MergeRequests, api: true do it "returns 401 if user has no permissions to merge" do user2 = create(:user) project.team << [user2, :reporter] - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user2) + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user2) expect(response).to have_http_status(401) expect(json_response['message']).to eq('401 Unauthorized') end it "returns 409 if the SHA parameter doesn't match" do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.diff_head_sha.reverse + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), sha: merge_request.diff_head_sha.reverse expect(response).to have_http_status(409) expect(json_response['message']).to start_with('SHA does not match HEAD of source branch') end it "succeeds if the SHA parameter matches" do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.diff_head_sha + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), sha: merge_request.diff_head_sha expect(response).to have_http_status(200) end @@ -483,18 +520,30 @@ 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.iid}/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 + + it "returns 404 for an invalid merge request IID" do + put api("/projects/#{project.id}/merge_requests/12345/merge", user) + + expect(response).to have_http_status(404) + end + + it "returns 404 if the merge request id is used instead of iid" do + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) + + expect(response).to have_http_status(404) end end - describe "PUT /projects/:id/merge_requests/:merge_request_id" do + describe "PUT /projects/:id/merge_requests/:merge_request_iid" do context "to close a MR" do it "returns merge_request" do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: "close" + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), state_event: "close" expect(response).to have_http_status(200) expect(json_response['state']).to eq('closed') @@ -502,38 +551,38 @@ describe API::MergeRequests, api: true do end it "updates title and returns merge_request" do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), title: "New title" + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), title: "New title" expect(response).to have_http_status(200) expect(json_response['title']).to eq('New title') end it "updates description and returns merge_request" do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), description: "New description" + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), description: "New description" expect(response).to have_http_status(200) expect(json_response['description']).to eq('New description') end it "updates milestone_id and returns merge_request" do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), milestone_id: milestone.id + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), milestone_id: milestone.id expect(response).to have_http_status(200) expect(json_response['milestone']['id']).to eq(milestone.id) end it "returns merge_request with renamed target_branch" do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), target_branch: "wiki" + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), target_branch: "wiki" expect(response).to have_http_status(200) expect(json_response['target_branch']).to eq('wiki') end it "returns merge_request that removes the source branch" do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), remove_source_branch: true + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), remove_source_branch: true expect(response).to have_http_status(200) expect(json_response['force_remove_source_branch']).to be_truthy end it 'allows special label names' do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), title: 'new issue', labels: 'label, label?, label&foo, ?, &' @@ -546,7 +595,7 @@ describe API::MergeRequests, api: true do end it 'does not update state when title is empty' do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', title: nil + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), state_event: 'close', title: nil merge_request.reload expect(response).to have_http_status(400) @@ -554,19 +603,31 @@ describe API::MergeRequests, api: true do end it 'does not update state when target_branch is empty' do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', target_branch: nil + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), state_event: 'close', target_branch: nil merge_request.reload expect(response).to have_http_status(400) expect(merge_request.state).to eq('opened') end + + it "returns 404 for an invalid merge request IID" do + put api("/projects/#{project.id}/merge_requests/12345", user), state_event: "close" + + expect(response).to have_http_status(404) + end + + it "returns 404 if the merge request id is used instead of iid" do + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: "close" + + expect(response).to have_http_status(404) + end end - describe "POST /projects/:id/merge_requests/:merge_request_id/comments" do + describe "POST /projects/:id/merge_requests/:merge_request_iid/comments" do it "returns comment" do original_count = merge_request.notes.size - post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user), note: "My comment" + post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/comments", user), note: "My comment" expect(response).to have_http_status(201) expect(json_response['note']).to eq('My comment') @@ -576,23 +637,29 @@ describe API::MergeRequests, api: true do end it "returns 400 if note is missing" do - post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user) + post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/comments", user) expect(response).to have_http_status(400) end - it "returns 404 if note is attached to non existent merge request" do + it "returns 404 if merge request iid is invalid" do post api("/projects/#{project.id}/merge_requests/404/comments", user), note: 'My comment' expect(response).to have_http_status(404) end + + it "returns 404 if merge request id is used instead of iid" do + post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user), + note: 'My comment' + expect(response).to have_http_status(404) + end end - describe "GET :id/merge_requests/:merge_request_id/comments" do + describe "GET :id/merge_requests/:merge_request_iid/comments" do let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") } let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") } it "returns merge_request comments ordered by created_at" do - get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user) + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/comments", user) expect(response).to have_http_status(200) expect(response).to include_pagination_headers @@ -603,20 +670,25 @@ describe API::MergeRequests, api: true do expect(json_response.last['note']).to eq("another comment on a MR") end - it "returns a 404 error if merge_request_id not found" do + it "returns a 404 error if merge_request_iid is invalid" do get api("/projects/#{project.id}/merge_requests/999/comments", user) expect(response).to have_http_status(404) end + + it "returns a 404 error if merge_request id is used instead of iid" do + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user) + expect(response).to have_http_status(404) + end end - describe 'GET :id/merge_requests/:merge_request_id/closes_issues' do + describe 'GET :id/merge_requests/:merge_request_iid/closes_issues' do it 'returns the issue that will be closed on merge' do issue = create(:issue, project: project) mr = merge_request.tap do |mr| mr.update_attribute(:description, "Closes #{issue.to_reference(mr.project)}") end - get api("/projects/#{project.id}/merge_requests/#{mr.id}/closes_issues", user) + get api("/projects/#{project.id}/merge_requests/#{mr.iid}/closes_issues", user) expect(response).to have_http_status(200) expect(response).to include_pagination_headers @@ -626,7 +698,7 @@ describe API::MergeRequests, api: true do end 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) + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/closes_issues", user) expect(response).to have_http_status(200) expect(response).to include_pagination_headers @@ -640,7 +712,7 @@ describe API::MergeRequests, api: true do merge_request = create(:merge_request, :simple, author: user, assignee: user, source_project: jira_project) merge_request.update_attribute(:description, "Closes #{issue.to_reference(jira_project)}") - get api("/projects/#{jira_project.id}/merge_requests/#{merge_request.id}/closes_issues", user) + get api("/projects/#{jira_project.id}/merge_requests/#{merge_request.iid}/closes_issues", user) expect(response).to have_http_status(200) expect(response).to include_pagination_headers @@ -656,22 +728,34 @@ describe API::MergeRequests, api: true do guest = create(:user) project.team << [guest, :guest] - get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", guest) + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/closes_issues", guest) expect(response).to have_http_status(403) end + + it "returns 404 for an invalid merge request IID" do + get api("/projects/#{project.id}/merge_requests/12345/closes_issues", user) + + expect(response).to have_http_status(404) + end + + it "returns 404 if the merge request id is used instead of iid" do + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", user) + + expect(response).to have_http_status(404) + end end - describe 'POST :id/merge_requests/:merge_request_id/subscribe' do + describe 'POST :id/merge_requests/:merge_request_iid/subscribe' do it 'subscribes to a merge request' do - post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscribe", admin) + post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/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}/subscribe", user) + post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/subscribe", user) expect(response).to have_http_status(304) end @@ -682,26 +766,32 @@ describe API::MergeRequests, api: true do expect(response).to have_http_status(404) end + it 'returns 404 if the merge request id is used instead of iid' do + post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscribe", user) + + expect(response).to have_http_status(404) + end + it 'returns 403 if user has no access to read code' do guest = create(:user) project.team << [guest, :guest] - post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscribe", guest) + post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/subscribe", guest) expect(response).to have_http_status(403) end end - describe 'POST :id/merge_requests/:merge_request_id/unsubscribe' do + describe 'POST :id/merge_requests/:merge_request_iid/unsubscribe' do it 'unsubscribes from a merge request' do - post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/unsubscribe", user) + post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/unsubscribe", user) expect(response).to have_http_status(201) expect(json_response['subscribed']).to eq(false) end it 'returns 304 if not subscribed' do - post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/unsubscribe", admin) + post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/unsubscribe", admin) expect(response).to have_http_status(304) end @@ -712,11 +802,17 @@ describe API::MergeRequests, api: true do expect(response).to have_http_status(404) end + it 'returns 404 if the merge request id is used instead of iid' do + post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/unsubscribe", user) + + expect(response).to have_http_status(404) + end + it 'returns 403 if user has no access to read code' do guest = create(:user) project.team << [guest, :guest] - post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/unsubscribe", guest) + post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/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 418bf5a507c..7fb728fed6f 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] } @@ -45,8 +45,37 @@ describe API::Milestones, api: true do expect(json_response.first['id']).to eq(closed_milestone.id) end - it 'returns a project milestone by iid' do - get api("/projects/#{project.id}/milestones?iid=#{closed_milestone.iid}", user) + 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 + 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 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 @@ -56,21 +85,22 @@ describe API::Milestones, api: true do 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(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 - 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 milestones by searching for description' do + get api("/projects/#{project.id}/milestones", user), search: 'open' expect(response).to have_http_status(200) expect(response).to include_pagination_headers - expect(json_response.size).to eq(2) + 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 @@ -197,6 +227,13 @@ describe API::Milestones, api: true do expect(json_response.first['milestone']['title']).to eq(milestone.title) end + it 'matches V4 response schema for a list of issues' do + get api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user) + + expect(response).to have_http_status(200) + expect(response).to match_response_schema('public_api/v4/issues') + end + it 'returns a 401 error if user not authenticated' do get api("/projects/#{project.id}/milestones/#{milestone.id}/issues") @@ -206,8 +243,8 @@ describe API::Milestones, api: true do 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) } + let(:issue) { create(:issue, project: public_project, position: 2) } + let(:confidential_issue) { create(:issue, confidential: true, project: public_project, position: 1) } before do public_project.team << [user, :developer] @@ -246,11 +283,24 @@ describe API::Milestones, api: true do expect(json_response.size).to eq(1) expect(json_response.map { |issue| issue['id'] }).to include(issue.id) end + + it 'returns issues ordered by position asc' 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.first['id']).to eq(confidential_issue.id) + expect(json_response.second['id']).to eq(issue.id) + end end end describe 'GET /projects/:id/milestones/:milestone_id/merge_requests' do - let(:merge_request) { create(:merge_request, source_project: project) } + let(:merge_request) { create(:merge_request, source_project: project, position: 2) } + let(:another_merge_request) { create(:merge_request, :simple, source_project: project, position: 1) } + before do milestone.merge_requests << merge_request end @@ -283,5 +333,18 @@ describe API::Milestones, api: true do expect(response).to have_http_status(401) end + + it 'returns merge_requests ordered by position asc' do + milestone.merge_requests << another_merge_request + + get api("/projects/#{project.id}/milestones/#{milestone.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.size).to eq(2) + expect(json_response.first['id']).to eq(another_merge_request.id) + expect(json_response.second['id']).to eq(merge_request.id) + end end end diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index 3cca4468be7..347f8f6fa3b 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -225,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 @@ -373,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) @@ -392,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) @@ -412,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/oauth_tokens_spec.rb b/spec/requests/api/oauth_tokens_spec.rb index 7e2cc50e591..367225df717 100644 --- a/spec/requests/api/oauth_tokens_spec.rb +++ b/spec/requests/api/oauth_tokens_spec.rb @@ -29,5 +29,27 @@ describe API::API, api: true do expect(json_response['access_token']).not_to be_nil end end + + context "when user is blocked" do + it "does not create an access token" do + user = create(:user) + user.block + + request_oauth_token(user) + + expect(response).to have_http_status(401) + end + end + + context "when user is ldap_blocked" do + it "does not create an access token" do + user = create(:user) + user.ldap_block + + request_oauth_token(user) + + expect(response).to have_http_status(401) + end + end end end diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb index 98d004b572e..51af999b455 100644 --- a/spec/requests/api/pipelines_spec.rb +++ b/spec/requests/api/pipelines_spec.rb @@ -24,6 +24,7 @@ describe API::Pipelines, api: true do 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 20c76bd2c05..b1f8c249092 100644 --- a/spec/requests/api/project_hooks_spec.rb +++ b/spec/requests/api/project_hooks_spec.rb @@ -33,7 +33,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do expect(json_response.first['merge_requests_events']).to eq(true) expect(json_response.first['tag_push_events']).to eq(true) expect(json_response.first['note_events']).to eq(true) - expect(json_response.first['build_events']).to eq(true) + expect(json_response.first['job_events']).to eq(true) expect(json_response.first['pipeline_events']).to eq(true) expect(json_response.first['wiki_page_events']).to eq(true) expect(json_response.first['enable_ssl_verification']).to eq(true) @@ -59,7 +59,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events) expect(json_response['tag_push_events']).to eq(hook.tag_push_events) expect(json_response['note_events']).to eq(hook.note_events) - expect(json_response['build_events']).to eq(hook.build_events) + expect(json_response['job_events']).to eq(hook.build_events) expect(json_response['pipeline_events']).to eq(hook.pipeline_events) expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events) expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification) @@ -98,7 +98,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do expect(json_response['merge_requests_events']).to eq(false) expect(json_response['tag_push_events']).to eq(false) expect(json_response['note_events']).to eq(false) - expect(json_response['build_events']).to eq(false) + expect(json_response['job_events']).to eq(false) expect(json_response['pipeline_events']).to eq(false) expect(json_response['wiki_page_events']).to eq(true) expect(json_response['enable_ssl_verification']).to eq(true) @@ -144,7 +144,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events) expect(json_response['tag_push_events']).to eq(hook.tag_push_events) expect(json_response['note_events']).to eq(hook.note_events) - expect(json_response['build_events']).to eq(hook.build_events) + expect(json_response['job_events']).to eq(hook.build_events) expect(json_response['pipeline_events']).to eq(hook.pipeline_events) expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events) expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification) @@ -183,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 da9df56401b..9e88c19b0bc 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -44,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 @@ -56,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 @@ -80,14 +80,14 @@ describe API::ProjectSnippets, api: true do context 'when the snippet is private' do it 'creates the snippet' do - expect { create_snippet(project, visibility_level: Snippet::PRIVATE) }. + expect { create_snippet(project, 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(project, visibility_level: Snippet::PUBLIC) }. + it 'rejects the snippet' do + expect { create_snippet(project, visibility: 'public') }. not_to change { Snippet.count } expect(response).to have_http_status(400) @@ -95,7 +95,7 @@ describe API::ProjectSnippets, api: true do end it 'creates a spam log' do - expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }. + expect { create_snippet(project, visibility: 'public') }. to change { SpamLog.count }.by(1) end end @@ -165,7 +165,7 @@ describe API::ProjectSnippets, api: true do let(:visibility_level) { Snippet::PRIVATE } it 'rejects the snippet' do - expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }. + expect { update_snippet(title: 'Foo', visibility: 'public') }. not_to change { snippet.reload.title } expect(response).to have_http_status(400) @@ -173,7 +173,7 @@ describe API::ProjectSnippets, api: true do end it 'creates a spam log' do - expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }. + expect { update_snippet(title: 'Foo', visibility: 'public') }. to change { SpamLog.count }.by(1) end end @@ -189,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 @@ -212,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 4e90aae9279..c481b7e72b1 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- require 'spec_helper' -describe API::Projects, api: true do - include ApiHelpers +describe API::Projects, :api do include Gitlab::CurrentSettings + let(:user) { create(:user) } let(:user2) { create(:user) } let(:user3) { create(:user) } @@ -43,9 +43,10 @@ describe API::Projects, api: true do describe 'GET /projects' do shared_examples_for 'projects response' do it 'returns an array of projects' do - get api('/projects', current_user) + 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 @@ -61,6 +62,7 @@ describe API::Projects, api: true do context 'when unauthenticated' do it_behaves_like 'projects response' do + let(:filter) { {} } let(:current_user) { nil } let(:projects) { [public_project] } end @@ -68,6 +70,7 @@ describe API::Projects, api: true do context 'when authenticated as regular user' do it_behaves_like 'projects response' do + let(:filter) { {} } let(:current_user) { user } let(:projects) { [public_project, project, project2, project3] } end @@ -121,7 +124,7 @@ describe API::Projects, api: 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) @@ -133,13 +136,18 @@ describe API::Projects, api: true do end context 'and using search' do - it 'returns searched project' do - get api('/projects', user), { search: project.name } + it_behaves_like 'projects response' do + let(:filter) { { search: project.name } } + let(:current_user) { user } + let(:projects) { [project] } + 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.length).to eq(1) + 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 @@ -216,36 +224,52 @@ describe API::Projects, api: true do end context 'and with all query parameters' do - # | | project5 | project6 | project7 | project8 | project9 | - # |---------+----------+----------+----------+----------+----------| - # | search | x | | x | x | x | - # | starred | x | x | | x | x | - # | public | x | x | x | | x | - # | owned | x | x | x | x | | - let!(:project5) { create(:empty_project, :public, path: 'gitlab5', namespace: user.namespace) } + let!(: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') } before do - user.update_attributes(starred_projects: [project5, project6, project8, project9]) + user.update_attributes(starred_projects: [project5, project7, project8, project9]) end - it 'returns only projects that satify all query parameters' do - get api('/projects', user), { visibility: 'public', owned: true, starred: true, search: 'gitlab' } + 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(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(project5.id) + 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 + + context 'including membership filter' do + before do + create(:project_member, + user: user, + project: project5, + access_level: ProjectMember::MASTER) + end + + it 'returns only projects that satisfy all query parameters' do + get api('/projects', user), { visibility: 'public', membership: true, starred: true, search: 'gitlab' } + + 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 'projects response' do + let(:filter) { {} } let(:current_user) { user2 } let(:projects) { [public_project] } end @@ -253,6 +277,7 @@ describe API::Projects, api: true do context 'when authenticated as admin' do it_behaves_like 'projects response' do + let(:filter) { {} } let(:current_user) { admin } let(:projects) { Project.all } end @@ -269,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 @@ -281,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 @@ -293,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 }) @@ -313,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 @@ -369,8 +424,16 @@ describe API::Projects, api: true do expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy end + it 'ignores import_url when it is nil' do + project = attributes_for(:project, { import_url: nil }) + + post api('/projects', user), project + + expect(response).to have_http_status(201) + 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]) @@ -388,10 +451,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 @@ -432,40 +492,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 @@ -529,9 +590,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 @@ -542,7 +602,7 @@ describe API::Projects, api: true do expect(json_response['issues_enabled']).to be_present expect(json_response['merge_requests_enabled']).to be_present expect(json_response['wiki_enabled']).to be_present - expect(json_response['builds_enabled']).to be_present + expect(json_response['jobs_enabled']).to be_present expect(json_response['snippets_enabled']).to be_present expect(json_response['container_registry_enabled']).to be_present expect(json_response['created_at']).to be_present @@ -553,13 +613,13 @@ describe API::Projects, api: true do expect(json_response['avatar_url']).to be_nil expect(json_response['star_count']).to be_present expect(json_response['forks_count']).to be_present - expect(json_response['public_builds']).to be_present + expect(json_response['public_jobs']).to be_present expect(json_response['shared_with_groups']).to be_an Array expect(json_response['shared_with_groups'].length).to eq(1) 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 @@ -785,8 +845,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 @@ -820,8 +879,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 @@ -905,8 +965,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 @@ -1035,7 +1097,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| @@ -1045,13 +1107,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 @@ -1118,7 +1180,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 @@ -1263,7 +1325,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 @@ -1287,7 +1351,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 @@ -1422,4 +1488,53 @@ describe API::Projects, api: true do 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 7652606a491..4783d011d54 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -30,7 +30,7 @@ describe API::Repositories, api: true do context 'when ref does not exist' do it_behaves_like '404 response' do - let(:request) { get api("#{route}?ref_name=foo", current_user) } + let(:request) { get api("#{route}?ref=foo", current_user) } let(:message) { '404 Tree Not Found' } end end @@ -66,7 +66,7 @@ describe API::Repositories, api: true do context 'when ref does not exist' do it_behaves_like '404 response' do - let(:request) { get api("#{route}?recursive=1&ref_name=foo", current_user) } + let(:request) { get api("#{route}?recursive=1&ref=foo", current_user) } let(:message) { '404 Tree Not Found' } end end @@ -100,82 +100,70 @@ describe API::Repositories, api: true do end end - { - 'blobs/:sha' => 'blobs/master', - 'commits/:sha/blob' => 'commits/master/blob' - }.each do |desc_path, example_path| - describe "GET /projects/:id/repository/#{desc_path}" do - let(:route) { "/projects/#{project.id}/repository/#{example_path}?filepath=README.md" } + describe "GET /projects/:id/repository/blobs/:sha" do + let(:route) { "/projects/#{project.id}/repository/blobs/#{sample_blob.oid}" } - shared_examples_for 'repository blob' do - it 'returns the repository blob' do - get api(route, current_user) - - expect(response).to have_http_status(200) - end - - context 'when sha does not exist' do - it_behaves_like '404 response' do - let(:request) { get api(route.sub('master', 'invalid_branch_name'), current_user) } - let(:message) { '404 Commit Not Found' } - end - end + shared_examples_for 'repository blob' do + it 'returns blob attributes as json' do + get api(route, current_user) - context 'when filepath does not exist' do - it_behaves_like '404 response' do - let(:request) { get api(route.sub('README.md', 'README.invalid'), current_user) } - let(:message) { '404 File Not Found' } - end - end + expect(response).to have_http_status(200) + expect(json_response['size']).to eq(111) + expect(json_response['encoding']).to eq("base64") + expect(Base64.decode64(json_response['content']).lines.first).to eq("class Commit\n") + expect(json_response['sha']).to eq(sample_blob.oid) + end - context 'when no filepath is given' do - it_behaves_like '400 response' do - let(:request) { get api(route.sub('?filepath=README.md', ''), current_user) } - end + context 'when sha does not exist' do + it_behaves_like '404 response' do + let(:request) { get api(route.sub(sample_blob.oid, '123456'), current_user) } + let(:message) { '404 Blob Not Found' } end + end - context 'when repository is disabled' do - include_context 'disabled repository' + context 'when repository is disabled' do + include_context 'disabled repository' - it_behaves_like '403 response' do - let(:request) { get api(route, current_user) } - end + it_behaves_like '403 response' do + let(:request) { get api(route, current_user) } end end + end - context 'when unauthenticated', 'and project is public' do - it_behaves_like 'repository blob' do - let(:project) { create(:project, :public, :repository) } - let(:current_user) { nil } - end + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository blob' 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 api(route) } - let(:message) { '404 Project Not Found' } - end + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get api(route) } + let(:message) { '404 Project Not Found' } end + end - context 'when authenticated', 'as a developer' do - it_behaves_like 'repository blob' do - let(:current_user) { user } - end + context 'when authenticated', 'as a developer' do + it_behaves_like 'repository blob' do + let(:current_user) { user } end + end - context 'when authenticated', 'as a guest' do - it_behaves_like '403 response' do - let(:request) { get api(route, guest) } - end + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get api(route, guest) } end end end - describe "GET /projects/:id/repository/raw_blobs/:sha" do - let(:route) { "/projects/#{project.id}/repository/raw_blobs/#{sample_blob.oid}" } + describe "GET /projects/:id/repository/blobs/:sha/raw" do + let(:route) { "/projects/#{project.id}/repository/blobs/#{sample_blob.oid}/raw" } shared_examples_for 'repository raw blob' do it 'returns the repository raw blob' do + expect(Gitlab::Workhorse).to receive(:send_git_blob) + get api(route, current_user) expect(response).to have_http_status(200) diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb new file mode 100644 index 00000000000..044b989e5ba --- /dev/null +++ b/spec/requests/api/runner_spec.rb @@ -0,0 +1,1072 @@ +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 + expect(runner.token).not_to eq(registration_token) + 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) + expect(Ci::Runner.first.token).not_to eq(registration_token) + expect(Ci::Runner.first.token).not_to eq(project.runners_token) + 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 "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 + + describe 'POST /api/v4/runners/verify' do + let(:runner) { create(:ci_runner) } + + context 'when no token is provided' do + it 'returns 400 error' do + post api('/runners/verify') + + expect(response).to have_http_status :bad_request + end + end + + context 'when invalid token is provided' do + it 'returns 403 error' do + post api('/runners/verify'), token: 'invalid-token' + + expect(response).to have_http_status 403 + end + end + + context 'when valid token is provided' do + it 'verifies Runner credentials' do + post api('/runners/verify'), token: runner.token + + expect(response).to have_http_status 200 + end + end + end + end + + describe '/api/v4/jobs' do + let(:project) { create(:empty_project, shared_runners_enabled: false) } + let(:pipeline) { create(:ci_pipeline_without_jobs, project: project, ref: 'master') } + let(:runner) { create(:ci_runner) } + let!(:job) do + create(:ci_build, :artifacts, :extended_options, + pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0, commands: "ls\ndate") + end + + before { project.runners << runner } + + describe 'POST /api/v4/jobs/request' do + let!(:last_update) {} + let!(:new_update) { } + let(:user_agent) { 'gitlab-runner 9.0.0 (9-0-stable; go1.7.4; linux/amd64)' } + + before { stub_container_registry_config(enabled: false) } + + shared_examples 'no jobs available' do + before { request_job } + + context 'when runner sends version in User-Agent' do + context 'for stable version' do + it 'gives 204 and set X-GitLab-Last-Update' do + expect(response).to have_http_status(204) + expect(response.header).to have_key('X-GitLab-Last-Update') + end + end + + context 'when last_update is up-to-date' do + let(:last_update) { runner.ensure_runner_queue_value } + + it 'gives 204 and set the same X-GitLab-Last-Update' do + expect(response).to have_http_status(204) + expect(response.header['X-GitLab-Last-Update']).to eq(last_update) + end + end + + context 'when last_update is outdated' do + let(:last_update) { runner.ensure_runner_queue_value } + let(:new_update) { runner.tick_runner_queue } + + it 'gives 204 and set a new X-GitLab-Last-Update' do + expect(response).to have_http_status(204) + expect(response.header['X-GitLab-Last-Update']).to eq(new_update) + end + end + + context 'when beta version is sent' do + let(:user_agent) { 'gitlab-runner 9.0.0~beta.167.g2b2bacc (master; go1.7.4; linux/amd64)' } + + it { expect(response).to have_http_status(204) } + end + + context 'when pre-9-0 version is sent' do + let(:user_agent) { 'gitlab-ci-multi-runner 1.6.0 (1-6-stable; go1.6.3; linux/amd64)' } + + it { expect(response).to have_http_status(204) } + end + + context 'when pre-9-0 beta version is sent' do + let(:user_agent) { 'gitlab-ci-multi-runner 1.6.0~beta.167.g2b2bacc (master; go1.6.3; linux/amd64)' } + + it { expect(response).to have_http_status(204) } + end + end + end + + context 'when no token is provided' do + it 'returns 400 error' do + post api('/jobs/request') + + expect(response).to have_http_status 400 + end + end + + context 'when invalid token is provided' do + it 'returns 403 error' do + post api('/jobs/request'), token: 'invalid' + + expect(response).to have_http_status 403 + end + end + + context 'when valid token is provided' do + context 'when Runner is not active' do + let(:runner) { create(:ci_runner, :inactive) } + + it 'returns 204 error' do + request_job + + expect(response).to have_http_status 204 + end + end + + context 'when jobs are finished' do + before { job.success } + + it_behaves_like 'no jobs available' + end + + context 'when other projects have pending jobs' do + before do + job.success + create(:ci_build, :pending) + end + + it_behaves_like 'no jobs available' + end + + context 'when shared runner requests job for project without shared_runners_enabled' do + let(:runner) { create(:ci_runner, :shared) } + + it_behaves_like 'no jobs available' + end + + context 'when there is a pending job' do + let(:expected_job_info) do + { 'name' => job.name, + 'stage' => job.stage, + 'project_id' => job.project.id, + 'project_name' => job.project.name } + end + + let(:expected_git_info) do + { 'repo_url' => job.repo_url, + 'ref' => job.ref, + 'sha' => job.sha, + 'before_sha' => job.before_sha, + 'ref_type' => 'branch' } + end + + let(:expected_steps) do + [{ 'name' => 'script', + 'script' => %w(ls date), + 'timeout' => job.timeout, + 'when' => 'on_success', + 'allow_failure' => false }, + { 'name' => 'after_script', + 'script' => %w(ls date), + 'timeout' => job.timeout, + 'when' => 'always', + 'allow_failure' => true }] + end + + let(:expected_variables) do + [{ 'key' => 'CI_JOB_NAME', 'value' => 'spinach', 'public' => true }, + { 'key' => 'CI_JOB_STAGE', 'value' => 'test', 'public' => true }, + { 'key' => 'DB_NAME', 'value' => 'postgres', 'public' => true }] + end + + let(:expected_artifacts) do + [{ 'name' => 'artifacts_file', + 'untracked' => false, + 'paths' => %w(out/), + 'when' => 'always', + 'expire_in' => '7d' }] + end + + let(:expected_cache) do + [{ 'key' => 'cache_key', + 'untracked' => false, + 'paths' => ['vendor/*'] }] + end + + it 'picks a job' do + request_job info: { platform: :darwin } + + expect(response).to have_http_status(201) + expect(response.headers).not_to have_key('X-GitLab-Last-Update') + expect(runner.reload.platform).to eq('darwin') + expect(json_response['id']).to eq(job.id) + expect(json_response['token']).to eq(job.token) + expect(json_response['job_info']).to eq(expected_job_info) + expect(json_response['git_info']).to eq(expected_git_info) + expect(json_response['image']).to eq({ 'name' => 'ruby:2.1' }) + expect(json_response['services']).to eq([{ 'name' => 'postgres' }]) + expect(json_response['steps']).to eq(expected_steps) + expect(json_response['artifacts']).to eq(expected_artifacts) + expect(json_response['cache']).to eq(expected_cache) + expect(json_response['variables']).to include(*expected_variables) + end + + context 'when job is made for tag' do + let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } + + it 'sets branch as ref_type' do + request_job + + expect(response).to have_http_status(201) + expect(json_response['git_info']['ref_type']).to eq('tag') + end + end + + context 'when job is made for branch' do + it 'sets tag as ref_type' do + request_job + + expect(response).to have_http_status(201) + expect(json_response['git_info']['ref_type']).to eq('branch') + end + end + + it 'updates runner info' do + expect { request_job }.to change { runner.reload.contacted_at } + end + + %w(name version revision platform architecture).each do |param| + context "when info parameter '#{param}' is present" do + let(:value) { "#{param}_value" } + + it "updates provided Runner's parameter" do + request_job info: { param => value } + + expect(response).to have_http_status(201) + expect(runner.reload.read_attribute(param.to_sym)).to eq(value) + end + end + end + + context 'when concurrently updating a job' do + before do + expect_any_instance_of(Ci::Build).to receive(:run!). + and_raise(ActiveRecord::StaleObjectError.new(nil, nil)) + end + + it 'returns a conflict' do + request_job + + expect(response).to have_http_status(409) + expect(response.headers).not_to have_key('X-GitLab-Last-Update') + end + end + + context 'when project and pipeline have multiple jobs' do + let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } + let!(:job2) { create(:ci_build_tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) } + let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) } + + before do + job.success + job2.success + end + + it 'returns dependent jobs' do + request_job + + expect(response).to have_http_status(201) + expect(json_response['id']).to eq(test_job.id) + expect(json_response['dependencies'].count).to eq(2) + expect(json_response['dependencies']).to include({ 'id' => job.id, 'name' => job.name, 'token' => job.token }, + { 'id' => job2.id, 'name' => job2.name, 'token' => job2.token }) + end + end + + context 'when explicit dependencies are defined' do + let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } + let!(:job2) { create(:ci_build_tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) } + let!(:test_job) do + create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'deploy', + stage: 'deploy', stage_idx: 1, + options: { dependencies: [job2.name] }) + end + + before do + job.success + job2.success + end + + it 'returns dependent jobs' do + request_job + + expect(response).to have_http_status(201) + expect(json_response['id']).to eq(test_job.id) + expect(json_response['dependencies'].count).to eq(1) + expect(json_response['dependencies'][0]).to include('id' => job2.id, 'name' => job2.name, 'token' => job2.token) + end + end + + context 'when job has no tags' do + before { job.update(tags: []) } + + context 'when runner is allowed to pick untagged jobs' do + before { runner.update_column(:run_untagged, true) } + + it 'picks job' do + request_job + + expect(response).to have_http_status 201 + end + end + + context 'when runner is not allowed to pick untagged jobs' do + before { runner.update_column(:run_untagged, false) } + + it_behaves_like 'no jobs available' + end + end + + context 'when triggered job is available' do + let(:expected_variables) do + [{ 'key' => 'CI_JOB_NAME', 'value' => 'spinach', 'public' => true }, + { 'key' => 'CI_JOB_STAGE', 'value' => 'test', 'public' => true }, + { 'key' => 'CI_PIPELINE_TRIGGERED', 'value' => 'true', 'public' => true }, + { 'key' => 'DB_NAME', 'value' => 'postgres', 'public' => true }, + { 'key' => 'SECRET_KEY', 'value' => 'secret_value', 'public' => false }, + { 'key' => 'TRIGGER_KEY_1', 'value' => 'TRIGGER_VALUE_1', 'public' => false }] + end + + before do + trigger = create(:ci_trigger, project: project) + create(:ci_trigger_request_with_variables, pipeline: pipeline, builds: [job], trigger: trigger) + project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value') + end + + it 'returns variables for triggers' do + request_job + + expect(response).to have_http_status(201) + expect(json_response['variables']).to include(*expected_variables) + end + end + + describe 'registry credentials support' do + let(:registry_url) { 'registry.example.com:5005' } + let(:registry_credentials) do + { 'type' => 'registry', + 'url' => registry_url, + 'username' => 'gitlab-ci-token', + 'password' => job.token } + end + + context 'when registry is enabled' do + before { stub_container_registry_config(enabled: true, host_port: registry_url) } + + it 'sends registry credentials key' do + request_job + + expect(json_response).to have_key('credentials') + expect(json_response['credentials']).to include(registry_credentials) + end + end + + context 'when registry is disabled' do + before { stub_container_registry_config(enabled: false, host_port: registry_url) } + + it 'does not send registry credentials' do + request_job + + expect(json_response).to have_key('credentials') + expect(json_response['credentials']).not_to include(registry_credentials) + end + end + end + end + + def request_job(token = runner.token, **params) + new_params = params.merge(token: token, last_update: last_update) + post api('/jobs/request'), new_params, { 'User-Agent' => user_agent } + end + end + end + + describe 'PUT /api/v4/jobs/:id' do + let(:job) { create(:ci_build, :pending, :trace, pipeline: pipeline, runner_id: runner.id) } + + before { job.run! } + + context 'when status is given' do + it 'mark job as succeeded' do + update_job(state: 'success') + + expect(job.reload.status).to eq 'success' + end + + it 'mark job as failed' do + update_job(state: 'failed') + + expect(job.reload.status).to eq 'failed' + end + end + + context 'when tace is given' do + it 'updates a running build' do + update_job(trace: 'BUILD TRACE UPDATED') + + expect(response).to have_http_status(200) + expect(job.reload.trace).to eq 'BUILD TRACE UPDATED' + end + end + + context 'when no trace is given' do + it 'does not override trace information' do + update_job + + expect(job.reload.trace).to eq 'BUILD TRACE' + end + end + + context 'when job has been erased' do + let(:job) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) } + + it 'responds with forbidden' do + update_job + + expect(response).to have_http_status(403) + end + end + + def update_job(token = job.token, **params) + new_params = params.merge(token: token) + put api("/jobs/#{job.id}"), new_params + end + end + + describe 'PATCH /api/v4/jobs/:id/trace' do + let(:job) { create(:ci_build, :running, :trace, runner_id: runner.id, pipeline: pipeline) } + let(:headers) { { API::Helpers::Runner::JOB_TOKEN_HEADER => job.token, 'Content-Type' => 'text/plain' } } + let(:headers_with_range) { headers.merge({ 'Content-Range' => '11-20' }) } + let(:update_interval) { 10.seconds.to_i } + + before { initial_patch_the_trace } + + context 'when request is valid' do + it 'gets correct response' do + expect(response.status).to eq 202 + expect(job.reload.trace).to eq 'BUILD TRACE appended' + expect(response.header).to have_key 'Range' + expect(response.header).to have_key 'Job-Status' + end + + context 'when job has been updated recently' do + it { expect{ patch_the_trace }.not_to change { job.updated_at }} + + it "changes the job's trace" do + patch_the_trace + + expect(job.reload.trace).to eq 'BUILD TRACE appended appended' + end + + context 'when Runner makes a force-patch' do + it { expect{ force_patch_the_trace }.not_to change { job.updated_at }} + + it "doesn't change the build.trace" do + force_patch_the_trace + + expect(job.reload.trace).to eq 'BUILD TRACE appended' + end + end + end + + context 'when job was not updated recently' do + let(:update_interval) { 15.minutes.to_i } + + it { expect { patch_the_trace }.to change { job.updated_at } } + + it 'changes the job.trace' do + patch_the_trace + + expect(job.reload.trace).to eq 'BUILD TRACE appended appended' + end + + context 'when Runner makes a force-patch' do + it { expect { force_patch_the_trace }.to change { job.updated_at } } + + it "doesn't change the job.trace" do + force_patch_the_trace + + expect(job.reload.trace).to eq 'BUILD TRACE appended' + end + end + end + + context 'when project for the build has been deleted' do + let(:job) do + create(:ci_build, :running, :trace, runner_id: runner.id, pipeline: pipeline) do |job| + job.project.update(pending_delete: true) + end + end + + it 'responds with forbidden' do + expect(response.status).to eq(403) + end + end + end + + context 'when Runner makes a force-patch' do + before do + force_patch_the_trace + end + + it 'gets correct response' do + expect(response.status).to eq 202 + expect(job.reload.trace).to eq 'BUILD TRACE appended' + expect(response.header).to have_key 'Range' + expect(response.header).to have_key 'Job-Status' + end + end + + context 'when content-range start is too big' do + let(:headers_with_range) { headers.merge({ 'Content-Range' => '15-20' }) } + + it 'gets 416 error response with range headers' do + expect(response.status).to eq 416 + expect(response.header).to have_key 'Range' + expect(response.header['Range']).to eq '0-11' + end + end + + context 'when content-range start is too small' do + let(:headers_with_range) { headers.merge({ 'Content-Range' => '8-20' }) } + + it 'gets 416 error response with range headers' do + expect(response.status).to eq 416 + expect(response.header).to have_key 'Range' + expect(response.header['Range']).to eq '0-11' + end + end + + context 'when Content-Range header is missing' do + let(:headers_with_range) { headers } + + it { expect(response.status).to eq 400 } + end + + context 'when job has been errased' do + let(:job) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) } + + it { expect(response.status).to eq 403 } + end + + def patch_the_trace(content = ' appended', request_headers = nil) + unless request_headers + offset = job.trace_length + limit = offset + content.length - 1 + request_headers = headers.merge({ 'Content-Range' => "#{offset}-#{limit}" }) + end + + Timecop.travel(job.updated_at + update_interval) do + patch api("/jobs/#{job.id}/trace"), content, request_headers + job.reload + end + end + + def initial_patch_the_trace + patch_the_trace(' appended', headers_with_range) + end + + def force_patch_the_trace + 2.times { patch_the_trace('') } + end + end + + describe 'artifacts' do + let(:job) { create(:ci_build, :pending, pipeline: pipeline, runner_id: runner.id) } + let(:jwt_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') } + let(:headers) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => jwt_token } } + let(:headers_with_token) { headers.merge(API::Helpers::Runner::JOB_TOKEN_HEADER => job.token) } + let(:file_upload) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } + let(:file_upload2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/gif') } + + before { job.run! } + + describe 'POST /api/v4/jobs/:id/artifacts/authorize' do + context 'when using token as parameter' do + it 'authorizes posting artifacts to running job' do + authorize_artifacts_with_token_in_params + + expect(response).to have_http_status(200) + expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + expect(json_response['TempPath']).not_to be_nil + end + + it 'fails to post too large artifact' do + stub_application_setting(max_artifacts_size: 0) + + authorize_artifacts_with_token_in_params(filesize: 100) + + expect(response).to have_http_status(413) + end + end + + context 'when using token as header' do + it 'authorizes posting artifacts to running job' do + authorize_artifacts_with_token_in_headers + + expect(response).to have_http_status(200) + expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + expect(json_response['TempPath']).not_to be_nil + end + + it 'fails to post too large artifact' do + stub_application_setting(max_artifacts_size: 0) + + authorize_artifacts_with_token_in_headers(filesize: 100) + + expect(response).to have_http_status(413) + end + end + + context 'when using runners token' do + it 'fails to authorize artifacts posting' do + authorize_artifacts(token: job.project.runners_token) + + expect(response).to have_http_status(403) + end + end + + it 'reject requests that did not go through gitlab-workhorse' do + headers.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER) + + authorize_artifacts + + expect(response).to have_http_status(500) + end + + context 'authorization token is invalid' do + it 'responds with forbidden' do + authorize_artifacts(token: 'invalid', filesize: 100 ) + + expect(response).to have_http_status(403) + end + end + + def authorize_artifacts(params = {}, request_headers = headers) + post api("/jobs/#{job.id}/artifacts/authorize"), params, request_headers + end + + def authorize_artifacts_with_token_in_params(params = {}, request_headers = headers) + params = params.merge(token: job.token) + authorize_artifacts(params, request_headers) + end + + def authorize_artifacts_with_token_in_headers(params = {}, request_headers = headers_with_token) + authorize_artifacts(params, request_headers) + end + end + + describe 'POST /api/v4/jobs/:id/artifacts' do + context 'when artifacts are being stored inside of tmp path' do + before do + # by configuring this path we allow to pass temp file from any path + allow(ArtifactUploader).to receive(:artifacts_upload_path).and_return('/') + end + + context 'when job has been erased' do + let(:job) { create(:ci_build, erased_at: Time.now) } + + before do + upload_artifacts(file_upload, headers_with_token) + end + + it 'responds with forbidden' do + upload_artifacts(file_upload, headers_with_token) + + expect(response).to have_http_status(403) + end + end + + context 'when job is running' do + shared_examples 'successful artifacts upload' do + it 'updates successfully' do + expect(response).to have_http_status(201) + end + end + + context 'when uses regular file post' do + before { upload_artifacts(file_upload, headers_with_token, false) } + + it_behaves_like 'successful artifacts upload' + end + + context 'when uses accelerated file post' do + before { upload_artifacts(file_upload, headers_with_token, true) } + + it_behaves_like 'successful artifacts upload' + end + + context 'when updates artifact' do + before do + upload_artifacts(file_upload2, headers_with_token) + upload_artifacts(file_upload, headers_with_token) + end + + it_behaves_like 'successful artifacts upload' + end + + context 'when using runners token' do + it 'responds with forbidden' do + upload_artifacts(file_upload, headers.merge(API::Helpers::Runner::JOB_TOKEN_HEADER => job.project.runners_token)) + + expect(response).to have_http_status(403) + end + end + end + + context 'when artifacts file is too large' do + it 'fails to post too large artifact' do + stub_application_setting(max_artifacts_size: 0) + + upload_artifacts(file_upload, headers_with_token) + + expect(response).to have_http_status(413) + end + end + + context 'when artifacts post request does not contain file' do + it 'fails to post artifacts without file' do + post api("/jobs/#{job.id}/artifacts"), {}, headers_with_token + + expect(response).to have_http_status(400) + end + end + + context 'GitLab Workhorse is not configured' do + it 'fails to post artifacts without GitLab-Workhorse' do + post api("/jobs/#{job.id}/artifacts"), { token: job.token }, {} + + expect(response).to have_http_status(403) + end + end + + context 'when setting an expire date' do + let(:default_artifacts_expire_in) {} + let(:post_data) do + { 'file.path' => file_upload.path, + 'file.name' => file_upload.original_filename, + 'expire_in' => expire_in } + end + + before do + stub_application_setting(default_artifacts_expire_in: default_artifacts_expire_in) + + post(api("/jobs/#{job.id}/artifacts"), post_data, headers_with_token) + end + + context 'when an expire_in is given' do + let(:expire_in) { '7 days' } + + it 'updates when specified' do + expect(response).to have_http_status(201) + expect(job.reload.artifacts_expire_at).to be_within(5.minutes).of(7.days.from_now) + end + end + + context 'when no expire_in is given' do + let(:expire_in) { nil } + + it 'ignores if not specified' do + expect(response).to have_http_status(201) + expect(job.reload.artifacts_expire_at).to be_nil + end + + context 'with application default' do + context 'when default is 5 days' do + let(:default_artifacts_expire_in) { '5 days' } + + it 'sets to application default' do + expect(response).to have_http_status(201) + expect(job.reload.artifacts_expire_at).to be_within(5.minutes).of(5.days.from_now) + end + end + + context 'when default is 0' do + let(:default_artifacts_expire_in) { '0' } + + it 'does not set expire_in' do + expect(response).to have_http_status(201) + expect(job.reload.artifacts_expire_at).to be_nil + end + end + end + end + end + + context 'posts artifacts file and metadata file' do + let!(:artifacts) { file_upload } + let!(:metadata) { file_upload2 } + + let(:stored_artifacts_file) { job.reload.artifacts_file.file } + let(:stored_metadata_file) { job.reload.artifacts_metadata.file } + let(:stored_artifacts_size) { job.reload.artifacts_size } + + before do + post(api("/jobs/#{job.id}/artifacts"), post_data, headers_with_token) + end + + context 'when posts data accelerated by workhorse is correct' do + let(:post_data) do + { 'file.path' => artifacts.path, + 'file.name' => artifacts.original_filename, + 'metadata.path' => metadata.path, + 'metadata.name' => metadata.original_filename } + end + + it 'stores artifacts and artifacts metadata' do + expect(response).to have_http_status(201) + expect(stored_artifacts_file.original_filename).to eq(artifacts.original_filename) + expect(stored_metadata_file.original_filename).to eq(metadata.original_filename) + expect(stored_artifacts_size).to eq(71759) + end + end + + context 'when there is no artifacts file in post data' do + let(:post_data) do + { 'metadata' => metadata } + end + + it 'is expected to respond with bad request' do + expect(response).to have_http_status(400) + end + + it 'does not store metadata' do + expect(stored_metadata_file).to be_nil + end + end + end + end + + context 'when artifacts are being stored outside of tmp path' do + before do + # by configuring this path we allow to pass file from @tmpdir only + # but all temporary files are stored in system tmp directory + @tmpdir = Dir.mktmpdir + allow(ArtifactUploader).to receive(:artifacts_upload_path).and_return(@tmpdir) + end + + after { FileUtils.remove_entry @tmpdir } + + it' "fails to post artifacts for outside of tmp path"' do + upload_artifacts(file_upload, headers_with_token) + + expect(response).to have_http_status(400) + end + end + + def upload_artifacts(file, headers = {}, accelerated = true) + params = if accelerated + { 'file.path' => file.path, 'file.name' => file.original_filename } + else + { 'file' => file } + end + post api("/jobs/#{job.id}/artifacts"), params, headers + end + end + + describe 'GET /api/v4/jobs/:id/artifacts' do + let(:token) { job.token } + + before { download_artifact } + + context 'when job has artifacts' do + let(:job) { create(:ci_build, :artifacts) } + let(:download_headers) do + { 'Content-Transfer-Encoding' => 'binary', + 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' } + end + + context 'when using job token' do + it 'download artifacts' do + expect(response).to have_http_status(200) + expect(response.headers).to include download_headers + end + end + + context 'when using runnners token' do + let(:token) { job.project.runners_token } + + it 'responds with forbidden' do + expect(response).to have_http_status(403) + end + end + end + + context 'when job does not has artifacts' do + it 'responds with not found' do + expect(response).to have_http_status(404) + end + end + + def download_artifact(params = {}, request_headers = headers) + params = params.merge(token: token) + get api("/jobs/#{job.id}/artifacts"), params, request_headers + end + end + end + end +end diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb index 103d6755888..8a82543a830 100644 --- a/spec/requests/api/runners_spec.rb +++ b/spec/requests/api/runners_spec.rb @@ -277,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 @@ -286,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 @@ -327,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 @@ -457,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/session_spec.rb b/spec/requests/api/session_spec.rb index 794e2b5c04d..28fab2011a5 100644 --- a/spec/requests/api/session_spec.rb +++ b/spec/requests/api/session_spec.rb @@ -87,5 +87,23 @@ describe API::Session, api: true do expect(response).to have_http_status(400) end end + + context "when user is blocked" do + it "returns authentication error" do + user.block + post api("/session"), email: user.username, password: user.password + + expect(response).to have_http_status(401) + end + end + + context "when user is ldap_blocked" do + it "returns authentication error" do + user.ldap_block + post api("/session"), email: user.username, password: user.password + + expect(response).to have_http_status(401) + end + end end 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 41def7cd1d4..5d75b47b3cd 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -74,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') @@ -87,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 @@ -120,14 +120,14 @@ 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) @@ -135,7 +135,7 @@ describe API::Snippets, api: true do 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 @@ -218,12 +218,12 @@ describe API::Snippets, api: true do let(:visibility_level) { Snippet::PRIVATE } it 'rejects the snippet' do - expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }. + 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_level: Snippet::PUBLIC) }. + expect { update_snippet(title: 'Foo', visibility: 'public') }. to change { SpamLog.count }.by(1) end end diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb index b59da632c00..d1e10f12657 100644 --- a/spec/requests/api/system_hooks_spec.rb +++ b/spec/requests/api/system_hooks_spec.rb @@ -91,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 8a4f078182f..b132d033a61 100644 --- a/spec/requests/api/tags_spec.rb +++ b/spec/requests/api/tags_spec.rb @@ -137,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 8506e8fccde..2c83e119065 100644 --- a/spec/requests/api/templates_spec.rb +++ b/spec/requests/api/templates_spec.rb @@ -58,11 +58,11 @@ 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 @@ -73,7 +73,7 @@ describe API::Templates, api: true 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(15) + expect(json_response.size).to eq(12) expect(json_response.map { |l| l['key'] }).to include('agpl-3.0') end @@ -102,7 +102,7 @@ describe API::Templates, api: true 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 diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb index f35e963a14b..b789284fa8d 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 @@ -163,7 +163,7 @@ describe API::Todos, api: true do shared_examples 'an issuable' do |issuable_type| it 'creates a todo on an issuable' do - post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.id}/todo", john_doe) + post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.iid}/todo", john_doe) expect(response.status).to eq(201) expect(json_response['project']).to be_a Hash @@ -180,7 +180,7 @@ describe API::Todos, api: true do it 'returns 304 there already exist a todo on that issuable' do create(:todo, project: project_1, author: author_1, user: john_doe, target: issuable) - post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.id}/todo", john_doe) + post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.iid}/todo", john_doe) expect(response.status).to eq(304) end @@ -195,7 +195,7 @@ describe API::Todos, api: true do guest = create(:user) project_1.team << [guest, :guest] - post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.id}/todo", guest) + post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.iid}/todo", guest) if issuable_type == 'merge_requests' expect(response).to have_http_status(403) diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb index 92dfc2aa277..d93a734f5b6 100644 --- a/spec/requests/api/triggers_spec.rb +++ b/spec/requests/api/triggers_spec.rb @@ -14,7 +14,7 @@ describe API::Triggers do let!(:trigger2) { create(:ci_trigger, project: project, token: trigger_token_2) } let!(:trigger_request) { create(:ci_trigger_request, trigger: trigger, created_at: '2015-01-01 12:13:14') } - describe 'POST /projects/:project_id/trigger' do + describe 'POST /projects/:project_id/trigger/pipeline' do let!(:project2) { create(:project) } let(:options) do { @@ -28,17 +28,20 @@ describe API::Triggers do context 'Handles errors' do it 'returns bad request if token is missing' do - post api("/projects/#{project.id}/trigger/builds"), ref: 'master' + post api("/projects/#{project.id}/trigger/pipeline"), ref: 'master' + expect(response).to have_http_status(400) end it 'returns not found if project is not found' do - post api('/projects/0/trigger/builds'), options.merge(ref: 'master') + post api('/projects/0/trigger/pipeline'), options.merge(ref: 'master') + expect(response).to have_http_status(404) end it 'returns unauthorized if token is for different project' do - post api("/projects/#{project2.id}/trigger/builds"), options.merge(ref: 'master') + post api("/projects/#{project2.id}/trigger/pipeline"), options.merge(ref: 'master') + expect(response).to have_http_status(401) end end @@ -46,25 +49,21 @@ describe API::Triggers do context 'Have a commit' do let(:pipeline) { project.pipelines.last } - it 'creates builds' do - post api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'master') + it 'creates pipeline' do + post api("/projects/#{project.id}/trigger/pipeline"), options.merge(ref: 'master') + expect(response).to have_http_status(201) + expect(json_response).to include('id' => pipeline.id) pipeline.builds.reload expect(pipeline.builds.pending.size).to eq(2) expect(pipeline.builds.size).to eq(5) end - it 'creates builds on webhook from other gitlab repository and branch' do - expect do - post api("/projects/#{project.id}/ref/master/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' } - end.to change(project.builds, :count).by(5) - expect(response).to have_http_status(201) - end + it 'returns bad request with no pipeline created if there\'s no commit for that ref' do + post api("/projects/#{project.id}/trigger/pipeline"), options.merge(ref: 'other-branch') - it 'returns bad request with no builds created if there\'s no commit for that ref' do - post api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'other-branch') expect(response).to have_http_status(400) - expect(json_response['message']).to eq('No builds created') + expect(json_response['message']).to eq('No pipeline created') end context 'Validates variables' do @@ -73,22 +72,46 @@ describe API::Triggers do end it 'validates variables to be a hash' do - post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: 'value', ref: 'master') + post api("/projects/#{project.id}/trigger/pipeline"), options.merge(variables: 'value', ref: 'master') + expect(response).to have_http_status(400) expect(json_response['error']).to eq('variables is invalid') end it 'validates variables needs to be a map of key-valued strings' do - post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: { key: %w(1 2) }, ref: 'master') + post api("/projects/#{project.id}/trigger/pipeline"), options.merge(variables: { key: %w(1 2) }, ref: 'master') + expect(response).to have_http_status(400) expect(json_response['message']).to eq('variables needs to be a map of key-valued strings') end it 'creates trigger request with variables' do - post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: variables, ref: 'master') + post api("/projects/#{project.id}/trigger/pipeline"), options.merge(variables: variables, ref: 'master') + + expect(response).to have_http_status(201) + expect(pipeline.builds.reload.first.trigger_request.variables).to eq(variables) + end + end + end + + context 'when triggering a pipeline from a trigger token' do + it 'creates builds from the ref given in the URL, not in the body' do + expect do + post api("/projects/#{project.id}/ref/master/trigger/pipeline?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' } + end.to change(project.builds, :count).by(5) + + expect(response).to have_http_status(201) + end + + context 'when ref contains a dot' do + it 'creates builds from the ref given in the URL, not in the body' do + project.repository.create_file(user, '.gitlab/gitlabhq/new_feature.md', 'something valid', message: 'new_feature', branch_name: 'v.1-branch') + + expect do + post api("/projects/#{project.id}/ref/v.1-branch/trigger/pipeline?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' } + end.to change(project.builds, :count).by(4) + expect(response).to have_http_status(201) - pipeline.builds.reload - expect(pipeline.builds.first.trigger_request.variables).to eq(variables) end end end @@ -123,17 +146,17 @@ describe API::Triggers do end end - describe 'GET /projects/:id/triggers/:token' do + describe 'GET /projects/:id/triggers/:trigger_id' do context 'authenticated user with valid permissions' do it 'returns trigger details' do - get api("/projects/#{project.id}/triggers/#{trigger.token}", user) + get api("/projects/#{project.id}/triggers/#{trigger.id}", user) expect(response).to have_http_status(200) expect(json_response).to be_a(Hash) end it 'responds with 404 Not Found if requesting non-existing trigger' do - get api("/projects/#{project.id}/triggers/abcdef012345", user) + get api("/projects/#{project.id}/triggers/-5", user) expect(response).to have_http_status(404) end @@ -141,7 +164,7 @@ describe API::Triggers do context 'authenticated user with invalid permissions' do it 'does not return triggers list' do - get api("/projects/#{project.id}/triggers/#{trigger.token}", user2) + get api("/projects/#{project.id}/triggers/#{trigger.id}", user2) expect(response).to have_http_status(403) end @@ -149,7 +172,7 @@ describe API::Triggers do context 'unauthenticated user' do it 'does not return triggers list' do - get api("/projects/#{project.id}/triggers/#{trigger.token}") + get api("/projects/#{project.id}/triggers/#{trigger.id}") expect(response).to have_http_status(401) end @@ -158,19 +181,31 @@ describe API::Triggers do describe 'POST /projects/:id/triggers' do context 'authenticated user with valid permissions' do - it 'creates trigger' do - expect do + context 'with required parameters' do + it 'creates trigger' do + expect do + post api("/projects/#{project.id}/triggers", user), + description: 'trigger' + end.to change{project.triggers.count}.by(1) + + expect(response).to have_http_status(201) + expect(json_response).to include('description' => 'trigger') + end + end + + context 'without required parameters' do + it 'does not create trigger' do post api("/projects/#{project.id}/triggers", user) - end.to change{project.triggers.count}.by(1) - expect(response).to have_http_status(201) - expect(json_response).to be_a(Hash) + expect(response).to have_http_status(:bad_request) + end end end context 'authenticated user with invalid permissions' do it 'does not create trigger' do - post api("/projects/#{project.id}/triggers", user2) + post api("/projects/#{project.id}/triggers", user2), + description: 'trigger' expect(response).to have_http_status(403) end @@ -178,24 +213,87 @@ describe API::Triggers do context 'unauthenticated user' do it 'does not create trigger' do - post api("/projects/#{project.id}/triggers") + post api("/projects/#{project.id}/triggers"), + description: 'trigger' expect(response).to have_http_status(401) end end end - describe 'DELETE /projects/:id/triggers/:token' do + describe 'PUT /projects/:id/triggers/:trigger_id' do + context 'authenticated user with valid permissions' do + let(:new_description) { 'new description' } + + it 'updates description' do + put api("/projects/#{project.id}/triggers/#{trigger.id}", user), + description: new_description + + expect(response).to have_http_status(200) + expect(json_response).to include('description' => new_description) + expect(trigger.reload.description).to eq(new_description) + end + end + + context 'authenticated user with invalid permissions' do + it 'does not update trigger' do + put api("/projects/#{project.id}/triggers/#{trigger.id}", user2) + + expect(response).to have_http_status(403) + end + end + + context 'unauthenticated user' do + it 'does not update trigger' do + put api("/projects/#{project.id}/triggers/#{trigger.id}") + + expect(response).to have_http_status(401) + end + end + end + + describe 'POST /projects/:id/triggers/:trigger_id/take_ownership' do + context 'authenticated user with valid permissions' do + it 'updates owner' do + expect(trigger.owner).to be_nil + + post api("/projects/#{project.id}/triggers/#{trigger.id}/take_ownership", user) + + expect(response).to have_http_status(200) + expect(json_response).to include('owner') + expect(trigger.reload.owner).to eq(user) + end + end + + context 'authenticated user with invalid permissions' do + it 'does not update owner' do + post api("/projects/#{project.id}/triggers/#{trigger.id}/take_ownership", user2) + + expect(response).to have_http_status(403) + end + end + + context 'unauthenticated user' do + it 'does not update owner' do + post api("/projects/#{project.id}/triggers/#{trigger.id}/take_ownership") + + expect(response).to have_http_status(401) + end + end + end + + describe 'DELETE /projects/:id/triggers/:trigger_id' do context 'authenticated user with valid permissions' do it 'deletes trigger' do expect do - delete api("/projects/#{project.id}/triggers/#{trigger.token}", user) + delete api("/projects/#{project.id}/triggers/#{trigger.id}", 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 - delete api("/projects/#{project.id}/triggers/abcdef012345", user) + delete api("/projects/#{project.id}/triggers/-5", user) expect(response).to have_http_status(404) end @@ -203,7 +301,7 @@ describe API::Triggers do context 'authenticated user with invalid permissions' do it 'does not delete trigger' do - delete api("/projects/#{project.id}/triggers/#{trigger.token}", user2) + delete api("/projects/#{project.id}/triggers/#{trigger.id}", user2) expect(response).to have_http_status(403) end @@ -211,7 +309,7 @@ describe API::Triggers do context 'unauthenticated user' do it 'does not delete trigger' do - delete api("/projects/#{project.id}/triggers/#{trigger.token}") + delete api("/projects/#{project.id}/triggers/#{trigger.id}") expect(response).to have_http_status(401) end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 603da9f49fc..04e7837fd7a 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -10,6 +10,8 @@ describe API::Users, api: true do let(:omniauth_user) { create(:omniauth_user) } let(:ldap_user) { create(:omniauth_user, provider: 'ldapmain') } let(:ldap_blocked_user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') } + let(:not_existing_user_id) { (User.maximum('id') || 0 ) + 10 } + let(:not_existing_pat_id) { (PersonalAccessToken.maximum('id') || 0 ) + 10 } describe "GET /users" do context "when unauthenticated" do @@ -540,10 +542,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 @@ -637,10 +641,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 @@ -671,10 +677,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 @@ -724,7 +730,7 @@ describe API::Users, api: true do get api("/user", user) expect(response).to have_http_status(200) - expect(response).to match_response_schema('user/public') + expect(response).to match_response_schema('public_api/v4/user/public') expect(json_response['id']).to eq(user.id) end end @@ -743,7 +749,7 @@ describe API::Users, api: true do get api("/user?private_token=#{admin_personal_access_token}") expect(response).to have_http_status(200) - expect(response).to match_response_schema('user/public') + expect(response).to match_response_schema('public_api/v4/user/public') expect(json_response['id']).to eq(admin.id) end end @@ -753,7 +759,7 @@ describe API::Users, api: true do get api("/user?private_token=#{admin.private_token}&sudo=#{user.id}") expect(response).to have_http_status(200) - expect(response).to match_response_schema('user/login') + expect(response).to match_response_schema('public_api/v4/user/login') expect(json_response['id']).to eq(user.id) end @@ -761,7 +767,7 @@ describe API::Users, api: true do get api("/user?private_token=#{admin.private_token}") expect(response).to have_http_status(200) - expect(response).to match_response_schema('user/public') + expect(response).to match_response_schema('public_api/v4/user/public') expect(json_response['id']).to eq(admin.id) end end @@ -869,10 +875,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 @@ -976,10 +984,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 @@ -1147,4 +1157,187 @@ describe API::Users, api: true do expect(json_response['message']).to eq('404 User Not Found') end end + + describe 'GET /users/:user_id/impersonation_tokens' do + let!(:active_personal_access_token) { create(:personal_access_token, user: user) } + let!(:revoked_personal_access_token) { create(:personal_access_token, :revoked, user: user) } + let!(:expired_personal_access_token) { create(:personal_access_token, :expired, user: user) } + let!(:impersonation_token) { create(:personal_access_token, :impersonation, user: user) } + let!(:revoked_impersonation_token) { create(:personal_access_token, :impersonation, :revoked, user: user) } + + it 'returns a 404 error if user not found' do + get api("/users/#{not_existing_user_id}/impersonation_tokens", admin) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 User Not Found') + end + + it 'returns a 403 error when authenticated as normal user' do + get api("/users/#{not_existing_user_id}/impersonation_tokens", user) + + expect(response).to have_http_status(403) + expect(json_response['message']).to eq('403 Forbidden') + end + + it 'returns an array of all impersonated tokens' do + get api("/users/#{user.id}/impersonation_tokens", admin) + + 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 + + it 'returns an array of active impersonation tokens if state active' do + get api("/users/#{user.id}/impersonation_tokens?state=active", admin) + + 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).to all(include('active' => true)) + end + + it 'returns an array of inactive personal access tokens if active is set to false' do + get api("/users/#{user.id}/impersonation_tokens?state=inactive", admin) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + expect(json_response).to all(include('active' => false)) + end + end + + describe 'POST /users/:user_id/impersonation_tokens' do + let(:name) { 'my new pat' } + let(:expires_at) { '2016-12-28' } + let(:scopes) { %w(api read_user) } + let(:impersonation) { true } + + it 'returns validation error if impersonation token misses some attributes' do + post api("/users/#{user.id}/impersonation_tokens", admin) + + expect(response).to have_http_status(400) + expect(json_response['error']).to eq('name is missing') + end + + it 'returns a 404 error if user not found' do + post api("/users/#{not_existing_user_id}/impersonation_tokens", admin), + name: name, + expires_at: expires_at + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 User Not Found') + end + + it 'returns a 403 error when authenticated as normal user' do + post api("/users/#{user.id}/impersonation_tokens", user), + name: name, + expires_at: expires_at + + expect(response).to have_http_status(403) + expect(json_response['message']).to eq('403 Forbidden') + end + + it 'creates a impersonation token' do + post api("/users/#{user.id}/impersonation_tokens", admin), + name: name, + expires_at: expires_at, + scopes: scopes, + impersonation: impersonation + + expect(response).to have_http_status(201) + expect(json_response['name']).to eq(name) + expect(json_response['scopes']).to eq(scopes) + expect(json_response['expires_at']).to eq(expires_at) + expect(json_response['id']).to be_present + expect(json_response['created_at']).to be_present + expect(json_response['active']).to be_falsey + expect(json_response['revoked']).to be_falsey + expect(json_response['token']).to be_present + expect(json_response['impersonation']).to eq(impersonation) + end + end + + describe 'GET /users/:user_id/impersonation_tokens/:impersonation_token_id' do + let!(:personal_access_token) { create(:personal_access_token, user: user) } + let!(:impersonation_token) { create(:personal_access_token, :impersonation, user: user) } + + it 'returns 404 error if user not found' do + get api("/users/#{not_existing_user_id}/impersonation_tokens/1", admin) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 User Not Found') + end + + it 'returns a 404 error if impersonation token not found' do + get api("/users/#{user.id}/impersonation_tokens/#{not_existing_pat_id}", admin) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Impersonation Token Not Found') + end + + it 'returns a 404 error if token is not impersonation token' do + get api("/users/#{user.id}/impersonation_tokens/#{personal_access_token.id}", admin) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Impersonation Token Not Found') + end + + it 'returns a 403 error when authenticated as normal user' do + get api("/users/#{user.id}/impersonation_tokens/#{impersonation_token.id}", user) + + expect(response).to have_http_status(403) + expect(json_response['message']).to eq('403 Forbidden') + end + + it 'returns a personal access token' do + get api("/users/#{user.id}/impersonation_tokens/#{impersonation_token.id}", admin) + + expect(response).to have_http_status(200) + expect(json_response['token']).to be_present + expect(json_response['impersonation']).to be_truthy + end + end + + describe 'DELETE /users/:user_id/impersonation_tokens/:impersonation_token_id' do + let!(:personal_access_token) { create(:personal_access_token, user: user) } + let!(:impersonation_token) { create(:personal_access_token, :impersonation, user: user) } + + it 'returns a 404 error if user not found' do + delete api("/users/#{not_existing_user_id}/impersonation_tokens/1", admin) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 User Not Found') + end + + it 'returns a 404 error if impersonation token not found' do + delete api("/users/#{user.id}/impersonation_tokens/#{not_existing_pat_id}", admin) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Impersonation Token Not Found') + end + + it 'returns a 404 error if token is not impersonation token' do + delete api("/users/#{user.id}/impersonation_tokens/#{personal_access_token.id}", admin) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Impersonation Token Not Found') + end + + it 'returns a 403 error when authenticated as normal user' do + delete api("/users/#{user.id}/impersonation_tokens/#{impersonation_token.id}", user) + + expect(response).to have_http_status(403) + expect(json_response['message']).to eq('403 Forbidden') + end + + it 'revokes a impersonation token' do + delete api("/users/#{user.id}/impersonation_tokens/#{impersonation_token.id}", admin) + + expect(response).to have_http_status(204) + expect(impersonation_token.revoked).to be_falsey + expect(impersonation_token.reload.revoked).to be_truthy + end + end end 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..eeb4d128c1b --- /dev/null +++ b/spec/requests/api/v3/award_emoji_spec.rb @@ -0,0 +1,299 @@ +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 "GET /projects/:id/awardable/:awardable_id/award_emoji" do + context 'on an issue' do + it "returns an array of award_emoji" do + get v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(award_emoji.name) + end + + it "returns a 404 error when issue id not found" do + get v3_api("/projects/#{project.id}/issues/12345/award_emoji", user) + + expect(response).to have_http_status(404) + end + end + + context 'on a merge request' do + it "returns an array of award_emoji" do + get v3_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 + end + + context 'on a snippet' do + let(:snippet) { create(:project_snippet, :public, project: project) } + let!(:award) { create(:award_emoji, awardable: snippet) } + + it 'returns the awarded emoji' do + get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(award.name) + end + end + + context 'when the user has no access' do + it 'returns a status code 404' do + user1 = create(:user) + + get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji", user1) + + expect(response).to have_http_status(404) + end + end + end + + describe 'GET /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji' do + let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket') } + + it 'returns an array of award emoji' do + get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(rocket.name) + end + end + + describe "GET /projects/:id/awardable/:awardable_id/award_emoji/:award_id" do + context 'on an issue' do + it "returns the award emoji" do + get v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/#{award_emoji.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(award_emoji.name) + expect(json_response['awardable_id']).to eq(issue.id) + expect(json_response['awardable_type']).to eq("Issue") + end + + it "returns a 404 error if the award is not found" do + get v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/12345", user) + + expect(response).to have_http_status(404) + end + end + + context 'on a merge request' do + it 'returns the award emoji' do + get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(downvote.name) + expect(json_response['awardable_id']).to eq(merge_request.id) + expect(json_response['awardable_type']).to eq("MergeRequest") + end + end + + context 'on a snippet' do + let(:snippet) { create(:project_snippet, :public, project: project) } + let!(:award) { create(:award_emoji, awardable: snippet) } + + it 'returns the awarded emoji' do + get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(award.name) + expect(json_response['awardable_id']).to eq(snippet.id) + expect(json_response['awardable_type']).to eq("Snippet") + end + end + + context 'when the user has no access' do + it 'returns a status code 404' do + user1 = create(:user) + + get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user1) + + expect(response).to have_http_status(404) + end + end + end + + describe 'GET /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji/:award_id' do + let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket') } + + it 'returns an award emoji' do + get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji/#{rocket.id}", user) + + expect(response).to have_http_status(200) + expect(json_response).not_to be_an Array + expect(json_response['name']).to eq(rocket.name) + end + end + + describe "POST /projects/:id/awardable/:awardable_id/award_emoji" do + let(:issue2) { create(:issue, project: project, author: user) } + + context "on an issue" do + it "creates a new award emoji" do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'blowfish' + + expect(response).to have_http_status(201) + expect(json_response['name']).to eq('blowfish') + expect(json_response['user']['username']).to eq(user.username) + end + + it "returns a 400 bad request error if the name is not given" do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user) + + expect(response).to have_http_status(400) + end + + it "returns a 401 unauthorized error if the user is not authenticated" do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji"), name: 'thumbsup' + + expect(response).to have_http_status(401) + end + + it "returns a 404 error if the user authored issue" do + post v3_api("/projects/#{project.id}/issues/#{issue2.id}/award_emoji", user), name: 'thumbsup' + + expect(response).to have_http_status(404) + end + + it "normalizes +1 as thumbsup award" do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: '+1' + + expect(issue.award_emoji.last.name).to eq("thumbsup") + end + + context 'when the emoji already has been awarded' do + it 'returns a 404 status code' do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'thumbsup' + post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'thumbsup' + + expect(response).to have_http_status(404) + expect(json_response["message"]).to match("has already been taken") + end + end + end + + context 'on a snippet' do + it 'creates a new award emoji' do + snippet = create(:project_snippet, :public, project: project) + + post v3_api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji", user), name: 'blowfish' + + expect(response).to have_http_status(201) + expect(json_response['name']).to eq('blowfish') + expect(json_response['user']['username']).to eq(user.username) + end + end + end + + describe "POST /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji" do + let(:note2) { create(:note, project: project, noteable: issue, author: user) } + + it 'creates a new award emoji' do + expect do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket' + end.to change { note.award_emoji.count }.from(0).to(1) + + expect(response).to have_http_status(201) + expect(json_response['user']['username']).to eq(user.username) + end + + it "it returns 404 error when user authored note" do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note2.id}/award_emoji", user), name: 'thumbsup' + + expect(response).to have_http_status(404) + end + + it "normalizes +1 as thumbsup award" do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: '+1' + + expect(note.award_emoji.last.name).to eq("thumbsup") + end + + context 'when the emoji already has been awarded' do + it 'returns a 404 status code' do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket' + post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket' + + expect(response).to have_http_status(404) + expect(json_response["message"]).to match("has already been taken") + end + end + end + + 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 index 8aaf3be4f87..eb95934f354 100644 --- a/spec/requests/api/v3/boards_spec.rb +++ b/spec/requests/api/v3/boards_spec.rb @@ -5,6 +5,7 @@ describe API::V3::Boards, api: true do 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 @@ -76,4 +77,37 @@ describe API::V3::Boards, api: true do 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 index 0e4c6bc3bc6..5dcd4f21f4e 100644 --- a/spec/requests/api/v3/branches_spec.rb +++ b/spec/requests/api/v3/branches_spec.rb @@ -5,8 +5,13 @@ 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_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } + 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 @@ -20,4 +25,111 @@ describe API::V3::Branches, api: true do 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 + + describe "POST /projects/:id/repository/branches" do + it "creates a new branch" do + post v3_api("/projects/#{project.id}/repository/branches", user), + branch_name: 'feature1', + ref: branch_sha + + expect(response).to have_http_status(201) + + expect(json_response['name']).to eq('feature1') + expect(json_response['commit']['id']).to eq(branch_sha) + end + + it "denies for user without push access" do + post v3_api("/projects/#{project.id}/repository/branches", user2), + branch_name: branch_name, + ref: branch_sha + expect(response).to have_http_status(403) + end + + it 'returns 400 if branch name is invalid' do + post v3_api("/projects/#{project.id}/repository/branches", user), + branch_name: 'new design', + ref: branch_sha + expect(response).to have_http_status(400) + expect(json_response['message']).to eq('Branch name is invalid') + end + + it 'returns 400 if branch already exists' do + post v3_api("/projects/#{project.id}/repository/branches", user), + branch_name: 'new_design1', + ref: branch_sha + expect(response).to have_http_status(201) + + post v3_api("/projects/#{project.id}/repository/branches", user), + branch_name: 'new_design1', + ref: branch_sha + + expect(response).to have_http_status(400) + expect(json_response['message']).to eq('Branch already exists') + end + + it 'returns 400 if ref name is invalid' do + post v3_api("/projects/#{project.id}/repository/branches", user), + branch_name: 'new_design3', + ref: 'foo' + + expect(response).to have_http_status(400) + expect(json_response['message']).to eq('Invalid reference name') + 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/builds_spec.rb b/spec/requests/api/v3/builds_spec.rb index 38aef7f2767..a50c22a6dd1 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/v3/builds_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::Builds, api: true do +describe API::V3::Builds, api: true do include ApiHelpers let(:user) { create(:user) } @@ -16,7 +16,9 @@ describe API::Builds, api: true do let(:query) { '' } before do - get api("/projects/#{project.id}/builds?#{query}", api_user) + create(:ci_build, :skipped, pipeline: pipeline) + + get v3_api("/projects/#{project.id}/builds?#{query}", api_user) end context 'authorized user' do @@ -49,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' } @@ -77,7 +91,7 @@ describe API::Builds, api: true do describe 'GET /projects/:id/repository/commits/:sha/builds' do context 'when commit does not exist in repository' do before do - get api("/projects/#{project.id}/repository/commits/1a271fd1/builds", api_user) + get v3_api("/projects/#{project.id}/repository/commits/1a271fd1/builds", api_user) end it 'responds with 404' do @@ -93,7 +107,7 @@ describe API::Builds, api: true do create(:ci_build, pipeline: pipeline) create(:ci_build) - get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", api_user) + get v3_api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", api_user) end it 'returns project jobs for specific commit' do @@ -116,7 +130,7 @@ describe API::Builds, api: true do context 'when pipeline has no jobs' do before do branch_head = project.commit('feature').id - get api("/projects/#{project.id}/repository/commits/#{branch_head}/builds", api_user) + get v3_api("/projects/#{project.id}/repository/commits/#{branch_head}/builds", api_user) end it 'returns an empty array' do @@ -132,7 +146,7 @@ describe API::Builds, api: true do create(:ci_pipeline, project: project, sha: project.commit.id) create(:ci_build, pipeline: pipeline) - get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", nil) + get v3_api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", nil) end it 'does not return project jobs' do @@ -145,7 +159,7 @@ describe API::Builds, api: true do describe 'GET /projects/:id/builds/:build_id' do before do - get api("/projects/#{project.id}/builds/#{build.id}", api_user) + get v3_api("/projects/#{project.id}/builds/#{build.id}", api_user) end context 'authorized user' do @@ -175,7 +189,7 @@ describe API::Builds, api: true do describe 'GET /projects/:id/builds/:build_id/artifacts' do before do - get api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user) + get v3_api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user) end context 'job with artifacts' do @@ -217,7 +231,7 @@ describe API::Builds, api: true do end def path_for_ref(ref = pipeline.ref, job = build.name) - api("/projects/#{project.id}/builds/artifacts/#{ref}/download?job=#{job}", api_user) + v3_api("/projects/#{project.id}/builds/artifacts/#{ref}/download?job=#{job}", api_user) end context 'when not logged in' do @@ -310,7 +324,7 @@ describe API::Builds, api: true do let(:build) { create(:ci_build, :trace, pipeline: pipeline) } before do - get api("/projects/#{project.id}/builds/#{build.id}/trace", api_user) + get v3_api("/projects/#{project.id}/builds/#{build.id}/trace", api_user) end context 'authorized user' do @@ -331,7 +345,7 @@ describe API::Builds, api: true do describe 'POST /projects/:id/builds/:build_id/cancel' do before do - post api("/projects/#{project.id}/builds/#{build.id}/cancel", api_user) + post v3_api("/projects/#{project.id}/builds/#{build.id}/cancel", api_user) end context 'authorized user' do @@ -364,7 +378,7 @@ describe API::Builds, api: true do let(:build) { create(:ci_build, :canceled, pipeline: pipeline) } before do - post api("/projects/#{project.id}/builds/#{build.id}/retry", api_user) + post v3_api("/projects/#{project.id}/builds/#{build.id}/retry", api_user) end context 'authorized user' do @@ -396,7 +410,7 @@ describe API::Builds, api: true do describe 'POST /projects/:id/builds/:build_id/erase' do before do - post api("/projects/#{project.id}/builds/#{build.id}/erase", user) + post v3_api("/projects/#{project.id}/builds/#{build.id}/erase", user) end context 'job is erasable' do @@ -426,7 +440,7 @@ describe API::Builds, api: true do describe 'POST /projects/:id/builds/:build_id/artifacts/keep' do before do - post api("/projects/#{project.id}/builds/#{build.id}/artifacts/keep", user) + post v3_api("/projects/#{project.id}/builds/#{build.id}/artifacts/keep", user) end context 'artifacts did not expire' do @@ -452,7 +466,7 @@ describe API::Builds, api: true do describe 'POST /projects/:id/builds/:build_id/play' do before do - post api("/projects/#{project.id}/builds/#{build.id}/play", user) + post v3_api("/projects/#{project.id}/builds/#{build.id}/play", user) end context 'on an playable job' do diff --git a/spec/requests/api/v3/commits_spec.rb b/spec/requests/api/v3/commits_spec.rb index 2d7584c3e59..adba3a787aa 100644 --- a/spec/requests/api/v3/commits_spec.rb +++ b/spec/requests/api/v3/commits_spec.rb @@ -88,7 +88,7 @@ describe API::V3::Commits, api: true do end end - describe "Create a commit with multiple files and actions" do + describe "POST /projects/:id/repository/commits" do let!(:url) { "/projects/#{project.id}/repository/commits" } it 'returns a 403 unauthorized for user without permissions' do @@ -103,7 +103,7 @@ describe API::V3::Commits, api: true do expect(response).to have_http_status(400) end - context :create do + describe 'create' do let(:message) { 'Created file' } let!(:invalid_c_params) do { @@ -147,8 +147,9 @@ describe API::V3::Commits, api: true do expect(response).to have_http_status(400) end - context 'with project path in URL' do - let(:url) { "/projects/#{project.namespace.path}%2F#{project.path}/repository/commits" } + context 'with project path containing a dot in URL' do + let!(:user) { create(:user, username: 'foo.bar') } + let(:url) { "/projects/#{CGI.escape(project.full_path)}/repository/commits" } it 'a new file in project repo' do post v3_api(url, user), valid_c_params @@ -158,7 +159,7 @@ describe API::V3::Commits, api: true do end end - context :delete do + describe 'delete' do let(:message) { 'Deleted file' } let!(:invalid_d_params) do { @@ -199,7 +200,7 @@ describe API::V3::Commits, api: true do end end - context :move do + describe 'move' do let(:message) { 'Moved file' } let!(:invalid_m_params) do { @@ -244,7 +245,7 @@ describe API::V3::Commits, api: true do end end - context :update do + describe 'update' do let(:message) { 'Updated file' } let!(:invalid_u_params) do { diff --git a/spec/requests/api/v3/deployments_spec.rb b/spec/requests/api/v3/deployments_spec.rb new file mode 100644 index 00000000000..3c5ce407b32 --- /dev/null +++ b/spec/requests/api/v3/deployments_spec.rb @@ -0,0 +1,71 @@ +require 'spec_helper' + +describe API::Deployments, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:non_member) { create(:user) } + let(:project) { deployment.environment.project } + let!(:deployment) { create(:deployment) } + + 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).to include_pagination_headers + end + end + + 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(json_response).to be_an Array + expect(json_response.size).to eq(1) + expect(json_response.first['iid']).to eq(deployment.iid) + expect(json_response.first['sha']).to match /\A\h{40}\z/ + end + end + + context 'as non member' do + it 'returns a 404 status code' do + get api("/projects/#{project.id}/deployments", non_member) + + expect(response).to have_http_status(404) + end + end + end + + describe 'GET /projects/:id/deployments/:deployment_id' do + context 'as a member of the project' do + it 'returns the projects deployment' do + get api("/projects/#{project.id}/deployments/#{deployment.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['sha']).to match /\A\h{40}\z/ + expect(json_response['id']).to eq(deployment.id) + end + end + + context 'as non member' do + it 'returns a 404 status code' do + get api("/projects/#{project.id}/deployments/#{deployment.id}", non_member) + + expect(response).to have_http_status(404) + 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 index 4af05605ec6..3b61139a2cd 100644 --- a/spec/requests/api/v3/files_spec.rb +++ b/spec/requests/api/v3/files_spec.rb @@ -2,17 +2,6 @@ require 'spec_helper' describe API::V3::Files, api: true do include ApiHelpers - 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 } # I have to remove periods from the end of the name # This happened when the user's name had a suffix (i.e. "Sr.") @@ -26,6 +15,18 @@ describe API::V3::Files, api: true do # ... # 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] } @@ -127,7 +128,7 @@ describe API::V3::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 v3_api("/projects/#{project.id}/repository/files", user), valid_params @@ -147,6 +148,20 @@ describe API::V3::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 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 @@ -215,7 +230,7 @@ describe API::V3::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 v3_api("/projects/#{project.id}/repository/files", user), valid_params 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 8e6732fe23e..51021eec63c 100644 --- a/spec/requests/api/v3/issues_spec.rb +++ b/spec/requests/api/v3/issues_spec.rb @@ -232,6 +232,13 @@ describe API::V3::Issues, api: true do expect(json_response).to be_an Array expect(response_dates).to eq(response_dates.sort) end + + it 'matches V3 response schema' do + get v3_api('/issues', user) + + expect(response).to have_http_status(200) + expect(response).to match_response_schema('public_api/v3/issues') + end end end @@ -432,6 +439,12 @@ describe API::V3::Issues, api: true do describe "GET /projects/:id/issues" do let(:base_url) { "/projects/#{project.id}" } + it 'returns 404 when project does not exist' do + get v3_api('/projects/1000/issues', non_member) + + expect(response).to have_http_status(404) + end + it "returns 404 on private projects for other users" do private_project = create(:empty_project, :private) create(:issue, project: private_project) @@ -722,7 +735,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 @@ -1281,6 +1294,6 @@ describe API::V3::Issues, api: true do describe 'time tracking endpoints' do let(:issuable) { issue } - include_examples 'time tracking endpoints', 'issue' + include_examples 'V3 time tracking endpoints', 'issue' end end diff --git a/spec/requests/api/v3/labels_spec.rb b/spec/requests/api/v3/labels_spec.rb index bcb0c6b9449..dfac357d37c 100644 --- a/spec/requests/api/v3/labels_spec.rb +++ b/spec/requests/api/v3/labels_spec.rb @@ -21,11 +21,11 @@ describe API::V3::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 v3_api("/projects/#{project.id}/labels", user) @@ -149,4 +149,23 @@ describe API::V3::Labels, api: true do 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 index 28c3ca03960..13814ed10c3 100644 --- a/spec/requests/api/v3/members_spec.rb +++ b/spec/requests/api/v3/members_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::Members, api: true do +describe API::V3::Members, api: true do include ApiHelpers let(:master) { create(:user) } diff --git a/spec/requests/api/v3/merge_request_diffs_spec.rb b/spec/requests/api/v3/merge_request_diffs_spec.rb new file mode 100644 index 00000000000..c53800eef30 --- /dev/null +++ b/spec/requests/api/v3/merge_request_diffs_spec.rb @@ -0,0 +1,50 @@ +require "spec_helper" + +describe API::V3::MergeRequestDiffs, 'MergeRequestDiffs', api: true do + include ApiHelpers + + let!(:user) { create(:user) } + let!(:merge_request) { create(:merge_request, importing: true) } + let!(:project) { merge_request.target_project } + + before do + merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') + merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') + project.team << [user, :master] + end + + describe 'GET /projects/:id/merge_requests/:merge_request_id/versions' do + it 'returns 200 for a valid merge request' do + get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions", user) + merge_request_diff = merge_request.merge_request_diffs.first + + expect(response.status).to eq 200 + 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) + end + + it 'returns a 404 when merge_request_id not found' do + get v3_api("/projects/#{project.id}/merge_requests/999/versions", user) + expect(response).to have_http_status(404) + end + end + + describe 'GET /projects/:id/merge_requests/:merge_request_id/versions/:version_id' do + it 'returns a 200 for a valid merge request' do + merge_request_diff = merge_request.merge_request_diffs.first + get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/#{merge_request_diff.id}", user) + + expect(response.status).to eq 200 + expect(json_response['id']).to eq(merge_request_diff.id) + expect(json_response['head_commit_sha']).to eq(merge_request_diff.head_commit_sha) + expect(json_response['diffs'].size).to eq(merge_request_diff.diffs.size) + end + + it 'returns a 404 when merge_request_id not found' do + get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/999", user) + + expect(response).to have_http_status(404) + end + end +end diff --git a/spec/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb index b94e1ef4ced..d73e9635c9b 100644 --- a/spec/requests/api/v3/merge_requests_spec.rb +++ b/spec/requests/api/v3/merge_requests_spec.rb @@ -73,6 +73,13 @@ describe API::MergeRequests, api: true do expect(json_response.first['title']).to eq(merge_request_merged.title) end + it 'matches V3 response schema' do + get v3_api("/projects/#{project.id}/merge_requests", user) + + expect(response).to have_http_status(200) + expect(response).to match_response_schema('public_api/v3/merge_requests') + end + context "with ordering" do before do @mr_later = mr_with_later_created_and_updated_at_time @@ -237,7 +244,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 @@ -705,7 +712,7 @@ describe API::MergeRequests, api: true do describe 'Time tracking' do let(:issuable) { merge_request } - include_examples 'time tracking endpoints', 'merge_request' + include_examples 'V3 time tracking endpoints', 'merge_request' end def mr_with_later_created_and_updated_at_time diff --git a/spec/requests/api/v3/milestones_spec.rb b/spec/requests/api/v3/milestones_spec.rb new file mode 100644 index 00000000000..127c0eec881 --- /dev/null +++ b/spec/requests/api/v3/milestones_spec.rb @@ -0,0 +1,239 @@ +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 'matches V3 response schema for a list of issues' do + get v3_api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user) + + expect(response).to have_http_status(200) + expect(response).to match_response_schema('public_api/v3/issues') + 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_hooks_spec.rb b/spec/requests/api/v3/project_hooks_spec.rb new file mode 100644 index 00000000000..a981119dc5a --- /dev/null +++ b/spec/requests/api/v3/project_hooks_spec.rb @@ -0,0 +1,216 @@ +require 'spec_helper' + +describe API::ProjectHooks, 'ProjectHooks', api: true do + include ApiHelpers + let(:user) { create(:user) } + let(:user3) { create(:user) } + let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } + let!(:hook) do + create(:project_hook, + :all_events_enabled, + project: project, + url: 'http://example.com', + enable_ssl_verification: true) + end + + before do + project.team << [user, :master] + project.team << [user3, :developer] + end + + describe "GET /projects/:id/hooks" do + context "authorized user" do + it "returns project hooks" do + get v3_api("/projects/#{project.id}/hooks", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + 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) + expect(json_response.first['push_events']).to eq(true) + expect(json_response.first['merge_requests_events']).to eq(true) + expect(json_response.first['tag_push_events']).to eq(true) + expect(json_response.first['note_events']).to eq(true) + expect(json_response.first['build_events']).to eq(true) + expect(json_response.first['pipeline_events']).to eq(true) + expect(json_response.first['wiki_page_events']).to eq(true) + expect(json_response.first['enable_ssl_verification']).to eq(true) + end + end + + context "unauthorized user" do + it "does not access project hooks" do + get v3_api("/projects/#{project.id}/hooks", user3) + + expect(response).to have_http_status(403) + end + end + end + + describe "GET /projects/:id/hooks/:hook_id" do + context "authorized user" do + it "returns a project hook" do + get v3_api("/projects/#{project.id}/hooks/#{hook.id}", user) + expect(response).to have_http_status(200) + expect(json_response['url']).to eq(hook.url) + expect(json_response['issues_events']).to eq(hook.issues_events) + expect(json_response['push_events']).to eq(hook.push_events) + expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events) + expect(json_response['tag_push_events']).to eq(hook.tag_push_events) + expect(json_response['note_events']).to eq(hook.note_events) + expect(json_response['build_events']).to eq(hook.build_events) + expect(json_response['pipeline_events']).to eq(hook.pipeline_events) + expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events) + expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification) + end + + it "returns a 404 error if hook id is not available" do + get v3_api("/projects/#{project.id}/hooks/1234", user) + expect(response).to have_http_status(404) + end + end + + context "unauthorized user" do + it "does not access an existing hook" do + get v3_api("/projects/#{project.id}/hooks/#{hook.id}", user3) + expect(response).to have_http_status(403) + end + end + + it "returns a 404 error if hook id is not available" do + get v3_api("/projects/#{project.id}/hooks/1234", user) + expect(response).to have_http_status(404) + end + end + + describe "POST /projects/:id/hooks" do + it "adds hook to project" do + expect do + post v3_api("/projects/#{project.id}/hooks", user), + url: "http://example.com", issues_events: true, wiki_page_events: true + end.to change {project.hooks.count}.by(1) + + expect(response).to have_http_status(201) + expect(json_response['url']).to eq('http://example.com') + expect(json_response['issues_events']).to eq(true) + expect(json_response['push_events']).to eq(true) + expect(json_response['merge_requests_events']).to eq(false) + expect(json_response['tag_push_events']).to eq(false) + expect(json_response['note_events']).to eq(false) + expect(json_response['build_events']).to eq(false) + expect(json_response['pipeline_events']).to eq(false) + expect(json_response['wiki_page_events']).to eq(true) + expect(json_response['enable_ssl_verification']).to eq(true) + expect(json_response).not_to include('token') + end + + it "adds the token without including it in the response" do + token = "secret token" + + expect do + post v3_api("/projects/#{project.id}/hooks", user), url: "http://example.com", token: token + end.to change {project.hooks.count}.by(1) + + expect(response).to have_http_status(201) + expect(json_response["url"]).to eq("http://example.com") + expect(json_response).not_to include("token") + + hook = project.hooks.find(json_response["id"]) + + expect(hook.url).to eq("http://example.com") + expect(hook.token).to eq(token) + end + + it "returns a 400 error if url not given" do + post v3_api("/projects/#{project.id}/hooks", user) + expect(response).to have_http_status(400) + end + + it "returns a 422 error if url not valid" do + post v3_api("/projects/#{project.id}/hooks", user), "url" => "ftp://example.com" + expect(response).to have_http_status(422) + end + end + + describe "PUT /projects/:id/hooks/:hook_id" do + it "updates an existing project hook" do + put v3_api("/projects/#{project.id}/hooks/#{hook.id}", user), + url: 'http://example.org', push_events: false + expect(response).to have_http_status(200) + expect(json_response['url']).to eq('http://example.org') + expect(json_response['issues_events']).to eq(hook.issues_events) + expect(json_response['push_events']).to eq(false) + expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events) + expect(json_response['tag_push_events']).to eq(hook.tag_push_events) + expect(json_response['note_events']).to eq(hook.note_events) + expect(json_response['build_events']).to eq(hook.build_events) + expect(json_response['pipeline_events']).to eq(hook.pipeline_events) + expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events) + expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification) + end + + it "adds the token without including it in the response" do + token = "secret token" + + put v3_api("/projects/#{project.id}/hooks/#{hook.id}", user), url: "http://example.org", token: token + + expect(response).to have_http_status(200) + expect(json_response["url"]).to eq("http://example.org") + expect(json_response).not_to include("token") + + expect(hook.reload.url).to eq("http://example.org") + expect(hook.reload.token).to eq(token) + end + + it "returns 404 error if hook id not found" do + put v3_api("/projects/#{project.id}/hooks/1234", user), url: 'http://example.org' + expect(response).to have_http_status(404) + end + + it "returns 400 error if url is not given" do + put v3_api("/projects/#{project.id}/hooks/#{hook.id}", user) + expect(response).to have_http_status(400) + end + + it "returns a 422 error if url is not valid" do + put v3_api("/projects/#{project.id}/hooks/#{hook.id}", user), url: 'ftp://example.com' + expect(response).to have_http_status(422) + end + end + + describe "DELETE /projects/:id/hooks/:hook_id" do + it "deletes hook from project" do + expect do + delete v3_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 v3_api("/projects/#{project.id}/hooks/#{hook.id}", user) + expect(response).to have_http_status(200) + end + + it "returns a 404 error when deleting non existent hook" do + delete v3_api("/projects/#{project.id}/hooks/42", user) + expect(response).to have_http_status(404) + end + + it "returns a 404 error if hook id not given" do + delete v3_api("/projects/#{project.id}/hooks", user) + + expect(response).to have_http_status(404) + end + + it "returns a 404 if a user attempts to delete project hooks he/she does not own" do + test_user = create(:user) + other_project = create(:project) + other_project.team << [test_user, :master] + + delete v3_api("/projects/#{other_project.id}/hooks/#{hook.id}", test_user) + expect(response).to have_http_status(404) + expect(WebHook.exists?(hook.id)).to be_truthy + end + end +end diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb index 36d99d80e79..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 diff --git a/spec/requests/api/v3/repositories_spec.rb b/spec/requests/api/v3/repositories_spec.rb index c696721c1c9..fef6fb641fa 100644 --- a/spec/requests/api/v3/repositories_spec.rb +++ b/spec/requests/api/v3/repositories_spec.rb @@ -3,6 +3,8 @@ require 'mime/types' describe API::V3::Repositories, api: true do include ApiHelpers + include RepoHelpers + include WorkhorseHelpers let(:user) { create(:user) } let(:guest) { create(:user).tap { |u| create(:project_member, :guest, user: u, project: project) } } @@ -96,6 +98,226 @@ describe API::V3::Repositories, api: true do end end + { + 'blobs/:sha' => 'blobs/master', + 'commits/:sha/blob' => 'commits/master/blob' + }.each do |desc_path, example_path| + describe "GET /projects/:id/repository/#{desc_path}" do + let(:route) { "/projects/#{project.id}/repository/#{example_path}?filepath=README.md" } + shared_examples_for 'repository blob' do + it 'returns the repository blob' do + get v3_api(route, current_user) + expect(response).to have_http_status(200) + end + context 'when sha does not exist' do + it_behaves_like '404 response' do + let(:request) { get v3_api(route.sub('master', 'invalid_branch_name'), current_user) } + let(:message) { '404 Commit Not Found' } + end + end + context 'when filepath does not exist' do + it_behaves_like '404 response' do + let(:request) { get v3_api(route.sub('README.md', 'README.invalid'), current_user) } + let(:message) { '404 File Not Found' } + end + end + context 'when no filepath is given' do + it_behaves_like '400 response' do + let(:request) { get v3_api(route.sub('?filepath=README.md', ''), current_user) } + 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 + end + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository blob' 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 blob' 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 + describe "GET /projects/:id/repository/raw_blobs/:sha" do + let(:route) { "/projects/#{project.id}/repository/raw_blobs/#{sample_blob.oid}" } + shared_examples_for 'repository raw blob' do + it 'returns the repository raw blob' do + get v3_api(route, current_user) + expect(response).to have_http_status(200) + end + context 'when sha does not exist' do + it_behaves_like '404 response' do + let(:request) { get v3_api(route.sub(sample_blob.oid, '123456'), current_user) } + let(:message) { '404 Blob 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 + end + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository raw blob' 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 raw blob' 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/archive(.:format)?:sha" do + let(:route) { "/projects/#{project.id}/repository/archive" } + shared_examples_for 'repository archive' do + it 'returns the repository archive' do + get v3_api(route, current_user) + expect(response).to have_http_status(200) + repo_name = project.repository.name.gsub("\.git", "") + type, params = workhorse_send_data + expect(type).to eq('git-archive') + expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.gz/) + end + it 'returns the repository archive archive.zip' do + get v3_api("/projects/#{project.id}/repository/archive.zip", user) + expect(response).to have_http_status(200) + repo_name = project.repository.name.gsub("\.git", "") + type, params = workhorse_send_data + expect(type).to eq('git-archive') + expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.zip/) + end + it 'returns the repository archive archive.tar.bz2' do + get v3_api("/projects/#{project.id}/repository/archive.tar.bz2", user) + expect(response).to have_http_status(200) + repo_name = project.repository.name.gsub("\.git", "") + type, params = workhorse_send_data + expect(type).to eq('git-archive') + expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.bz2/) + end + context 'when sha does not exist' do + it_behaves_like '404 response' do + let(:request) { get v3_api("#{route}?sha=xxx", current_user) } + let(:message) { '404 File Not Found' } + end + end + end + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository archive' 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 archive' 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/compare' do + let(:route) { "/projects/#{project.id}/repository/compare" } + shared_examples_for 'repository compare' do + it "compares branches" do + get v3_api(route, current_user), from: 'master', to: 'feature' + expect(response).to have_http_status(200) + expect(json_response['commits']).to be_present + expect(json_response['diffs']).to be_present + end + it "compares tags" do + get v3_api(route, current_user), from: 'v1.0.0', to: 'v1.1.0' + expect(response).to have_http_status(200) + expect(json_response['commits']).to be_present + expect(json_response['diffs']).to be_present + end + it "compares commits" do + get v3_api(route, current_user), from: sample_commit.id, to: sample_commit.parent_id + expect(response).to have_http_status(200) + expect(json_response['commits']).to be_empty + expect(json_response['diffs']).to be_empty + expect(json_response['compare_same_ref']).to be_falsey + end + it "compares commits in reverse order" do + get v3_api(route, current_user), from: sample_commit.parent_id, to: sample_commit.id + expect(response).to have_http_status(200) + expect(json_response['commits']).to be_present + expect(json_response['diffs']).to be_present + end + it "compares same refs" do + get v3_api(route, current_user), from: 'master', to: 'master' + expect(response).to have_http_status(200) + expect(json_response['commits']).to be_empty + expect(json_response['diffs']).to be_empty + expect(json_response['compare_same_ref']).to be_truthy + end + end + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository compare' 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 compare' 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" } 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..3a760a8f25c --- /dev/null +++ b/spec/requests/api/v3/services_spec.rb @@ -0,0 +1,24 @@ +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) } + + available_services = Service.available_services_names + available_services.delete('prometheus') + available_services.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 index da58efb6ebf..91038977c82 100644 --- a/spec/requests/api/v3/system_hooks_spec.rb +++ b/spec/requests/api/v3/system_hooks_spec.rb @@ -38,4 +38,20 @@ describe API::V3::SystemHooks, api: true do 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 index 6722789d928..6870cfd2668 100644 --- a/spec/requests/api/v3/tags_spec.rb +++ b/spec/requests/api/v3/tags_spec.rb @@ -64,4 +64,26 @@ describe API::V3::Tags, api: true do 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 index 4fd4e70bedd..f1e554b98cc 100644 --- a/spec/requests/api/v3/templates_spec.rb +++ b/spec/requests/api/v3/templates_spec.rb @@ -56,11 +56,11 @@ describe API::V3::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 @@ -70,7 +70,7 @@ describe API::V3::Templates, api: true do expect(response).to have_http_status(200) 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 @@ -98,7 +98,7 @@ describe API::V3::Templates, api: true 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 diff --git a/spec/requests/api/v3/triggers_spec.rb b/spec/requests/api/v3/triggers_spec.rb new file mode 100644 index 00000000000..9233e9621bf --- /dev/null +++ b/spec/requests/api/v3/triggers_spec.rb @@ -0,0 +1,232 @@ +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 'POST /projects/:project_id/trigger' do + let!(:project2) { create(:project) } + let(:options) do + { + token: trigger_token + } + end + + before do + stub_ci_pipeline_to_return_yaml_file + end + + context 'Handles errors' do + it 'returns bad request if token is missing' do + post v3_api("/projects/#{project.id}/trigger/builds"), ref: 'master' + expect(response).to have_http_status(400) + end + + it 'returns not found if project is not found' do + post v3_api('/projects/0/trigger/builds'), options.merge(ref: 'master') + expect(response).to have_http_status(404) + end + + it 'returns unauthorized if token is for different project' do + post v3_api("/projects/#{project2.id}/trigger/builds"), options.merge(ref: 'master') + expect(response).to have_http_status(401) + end + end + + context 'Have a commit' do + let(:pipeline) { project.pipelines.last } + + it 'creates builds' do + post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'master') + expect(response).to have_http_status(201) + pipeline.builds.reload + expect(pipeline.builds.pending.size).to eq(2) + expect(pipeline.builds.size).to eq(5) + end + + it 'returns bad request with no builds created if there\'s no commit for that ref' do + post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'other-branch') + expect(response).to have_http_status(400) + expect(json_response['message']).to eq('No builds created') + end + + context 'Validates variables' do + let(:variables) do + { 'TRIGGER_KEY' => 'TRIGGER_VALUE' } + end + + it 'validates variables to be a hash' do + post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(variables: 'value', ref: 'master') + expect(response).to have_http_status(400) + expect(json_response['error']).to eq('variables is invalid') + end + + it 'validates variables needs to be a map of key-valued strings' do + post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(variables: { key: %w(1 2) }, ref: 'master') + expect(response).to have_http_status(400) + expect(json_response['message']).to eq('variables needs to be a map of key-valued strings') + end + + it 'creates trigger request with variables' do + post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(variables: variables, ref: 'master') + expect(response).to have_http_status(201) + pipeline.builds.reload + expect(pipeline.builds.first.trigger_request.variables).to eq(variables) + end + end + end + + context 'when triggering a pipeline from a trigger token' do + it 'creates builds from the ref given in the URL, not in the body' do + expect do + post v3_api("/projects/#{project.id}/ref/master/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' } + end.to change(project.builds, :count).by(5) + expect(response).to have_http_status(201) + end + + context 'when ref contains a dot' do + it 'creates builds from the ref given in the URL, not in the body' do + project.repository.create_file(user, '.gitlab/gitlabhq/new_feature.md', 'something valid', message: 'new_feature', branch_name: 'v.1-branch') + + expect do + post v3_api("/projects/#{project.id}/ref/v.1-branch/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' } + end.to change(project.builds, :count).by(4) + + expect(response).to have_http_status(201) + end + end + end + end + + describe 'GET /projects/:id/triggers' do + context 'authenticated user with valid permissions' do + it 'returns list of triggers' do + get v3_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 + end + + context 'authenticated user with invalid permissions' do + it 'does not return triggers list' do + get v3_api("/projects/#{project.id}/triggers", user2) + + expect(response).to have_http_status(403) + end + end + + context 'unauthenticated user' do + it 'does not return triggers list' do + get v3_api("/projects/#{project.id}/triggers") + + expect(response).to have_http_status(401) + end + end + end + + describe 'GET /projects/:id/triggers/:token' do + context 'authenticated user with valid permissions' do + it 'returns trigger details' do + get v3_api("/projects/#{project.id}/triggers/#{trigger.token}", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_a(Hash) + end + + it 'responds with 404 Not Found if requesting non-existing trigger' do + get 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 return triggers list' do + get 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 return triggers list' do + get v3_api("/projects/#{project.id}/triggers/#{trigger.token}") + + expect(response).to have_http_status(401) + end + end + end + + describe 'POST /projects/:id/triggers' do + context 'authenticated user with valid permissions' do + it 'creates trigger' do + expect do + post v3_api("/projects/#{project.id}/triggers", user) + end.to change{project.triggers.count}.by(1) + + expect(response).to have_http_status(201) + expect(json_response).to be_a(Hash) + end + end + + context 'authenticated user with invalid permissions' do + it 'does not create trigger' do + post v3_api("/projects/#{project.id}/triggers", user2) + + expect(response).to have_http_status(403) + end + end + + context 'unauthenticated user' do + it 'does not create trigger' do + post v3_api("/projects/#{project.id}/triggers") + + expect(response).to have_http_status(401) + end + end + end + + 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 index 5020ef18a3a..17bbb0b53c1 100644 --- a/spec/requests/api/v3/users_spec.rb +++ b/spec/requests/api/v3/users_spec.rb @@ -186,4 +186,81 @@ describe API::V3::Users, api: true do 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 |