diff options
Diffstat (limited to 'spec/requests/api')
35 files changed, 2105 insertions, 454 deletions
diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb new file mode 100644 index 00000000000..d78494b76fa --- /dev/null +++ b/spec/requests/api/access_requests_spec.rb @@ -0,0 +1,246 @@ +require 'spec_helper' + +describe API::AccessRequests, api: true do + include ApiHelpers + + let(:master) { create(:user) } + let(:developer) { create(:user) } + let(:access_requester) { create(:user) } + let(:stranger) { create(:user) } + + let(:project) do + project = create(:project, :public, creator_id: master.id, namespace: master.namespace) + project.team << [developer, :developer] + project.team << [master, :master] + project.request_access(access_requester) + project + end + + let(:group) do + group = create(:group, :public) + group.add_developer(developer) + group.add_owner(master) + group.request_access(access_requester) + group + end + + shared_examples 'GET /:sources/:id/access_requests' do |source_type| + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) { get api("/#{source_type.pluralize}/#{source.id}/access_requests", stranger) } + end + + context 'when authenticated as a non-master/owner' do + %i[developer access_requester stranger].each do |type| + context "as a #{type}" do + it 'returns 403' do + user = public_send(type) + get api("/#{source_type.pluralize}/#{source.id}/access_requests", user) + + expect(response).to have_http_status(403) + end + end + end + end + + context 'when authenticated as a master/owner' do + it 'returns access requesters' do + get api("/#{source_type.pluralize}/#{source.id}/access_requests", master) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + end + end + end + end + + shared_examples 'POST /:sources/:id/access_requests' do |source_type| + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) { post api("/#{source_type.pluralize}/#{source.id}/access_requests", stranger) } + end + + context 'when authenticated as a member' do + %i[developer master].each do |type| + context "as a #{type}" do + it 'returns 400' do + expect do + user = public_send(type) + post api("/#{source_type.pluralize}/#{source.id}/access_requests", user) + + expect(response).to have_http_status(400) + end.not_to change { source.requesters.count } + end + end + end + end + + context 'when authenticated as an access requester' do + it 'returns 400' do + expect do + post api("/#{source_type.pluralize}/#{source.id}/access_requests", access_requester) + + expect(response).to have_http_status(400) + end.not_to change { source.requesters.count } + end + end + + context 'when authenticated as a stranger' do + it 'returns 201' do + expect do + post api("/#{source_type.pluralize}/#{source.id}/access_requests", stranger) + + expect(response).to have_http_status(201) + end.to change { source.requesters.count }.by(1) + + # User attributes + expect(json_response['id']).to eq(stranger.id) + expect(json_response['name']).to eq(stranger.name) + expect(json_response['username']).to eq(stranger.username) + expect(json_response['state']).to eq(stranger.state) + expect(json_response['avatar_url']).to eq(stranger.avatar_url) + expect(json_response['web_url']).to eq(Gitlab::Routing.url_helpers.user_url(stranger)) + + # Member attributes + expect(json_response['requested_at']).to be_present + end + end + end + end + + shared_examples 'PUT /:sources/:id/access_requests/:user_id/approve' do |source_type| + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) { put api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}/approve", stranger) } + end + + context 'when authenticated as a non-master/owner' do + %i[developer access_requester stranger].each do |type| + context "as a #{type}" do + it 'returns 403' do + user = public_send(type) + put api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}/approve", user) + + expect(response).to have_http_status(403) + end + end + end + end + + context 'when authenticated as a master/owner' do + it 'returns 201' do + expect do + put api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}/approve", master), + access_level: Member::MASTER + + expect(response).to have_http_status(201) + end.to change { source.members.count }.by(1) + # User attributes + expect(json_response['id']).to eq(access_requester.id) + expect(json_response['name']).to eq(access_requester.name) + expect(json_response['username']).to eq(access_requester.username) + expect(json_response['state']).to eq(access_requester.state) + expect(json_response['avatar_url']).to eq(access_requester.avatar_url) + expect(json_response['web_url']).to eq(Gitlab::Routing.url_helpers.user_url(access_requester)) + + # Member attributes + expect(json_response['access_level']).to eq(Member::MASTER) + end + + context 'user_id does not match an existing access requester' do + it 'returns 404' do + expect do + put api("/#{source_type.pluralize}/#{source.id}/access_requests/#{stranger.id}/approve", master) + + expect(response).to have_http_status(404) + end.not_to change { source.members.count } + end + end + end + end + end + + shared_examples 'DELETE /:sources/:id/access_requests/:user_id' do |source_type| + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) { delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", stranger) } + end + + context 'when authenticated as a non-master/owner' do + %i[developer stranger].each do |type| + context "as a #{type}" do + it 'returns 403' do + user = public_send(type) + delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", user) + + expect(response).to have_http_status(403) + end + end + end + end + + context 'when authenticated as the access requester' do + it 'returns 200' do + expect do + delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", access_requester) + + expect(response).to have_http_status(200) + end.to change { source.requesters.count }.by(-1) + end + end + + context 'when authenticated as a master/owner' do + it 'returns 200' do + expect do + delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", master) + + expect(response).to have_http_status(200) + end.to change { source.requesters.count }.by(-1) + end + + context 'user_id does not match an existing access requester' do + it 'returns 404' do + expect do + delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{stranger.id}", master) + + expect(response).to have_http_status(404) + end.not_to change { source.requesters.count } + end + end + end + end + end + + it_behaves_like 'GET /:sources/:id/access_requests', 'project' do + let(:source) { project } + end + + it_behaves_like 'GET /:sources/:id/access_requests', 'group' do + let(:source) { group } + end + + it_behaves_like 'POST /:sources/:id/access_requests', 'project' do + let(:source) { project } + end + + it_behaves_like 'POST /:sources/:id/access_requests', 'group' do + let(:source) { group } + end + + it_behaves_like 'PUT /:sources/:id/access_requests/:user_id/approve', 'project' do + let(:source) { project } + end + + it_behaves_like 'PUT /:sources/:id/access_requests/:user_id/approve', 'group' do + let(:source) { group } + end + + it_behaves_like 'DELETE /:sources/:id/access_requests/:user_id', 'project' do + let(:source) { project } + end + + it_behaves_like 'DELETE /:sources/:id/access_requests/:user_id', 'group' do + let(:source) { group } + end +end diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb index c65510fadec..e66faeed705 100644 --- a/spec/requests/api/api_helpers_spec.rb +++ b/spec/requests/api/api_helpers_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' describe API::Helpers, api: true do include API::Helpers include ApiHelpers + include SentryHelper let(:user) { create(:user) } let(:admin) { create(:admin) } @@ -35,11 +36,36 @@ describe API::Helpers, api: true do params.delete(API::Helpers::SUDO_PARAM) end + def warden_authenticate_returns(value) + warden = double("warden", authenticate: value) + env['warden'] = warden + end + + def doorkeeper_guard_returns(value) + allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ value } + end + def error!(message, status) raise Exception end describe ".current_user" do + subject { current_user } + + describe "when authenticating via Warden" do + before { doorkeeper_guard_returns false } + + context "fails" do + it { is_expected.to be_nil } + end + + context "succeeds" do + before { warden_authenticate_returns user } + + it { is_expected.to eq(user) } + end + end + describe "when authenticating using a user's private token" do it "returns nil for an invalid token" do env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token' @@ -234,4 +260,30 @@ describe API::Helpers, api: true do expect(to_boolean(nil)).to be_nil end end + + describe '.handle_api_exception' do + before do + allow_any_instance_of(self.class).to receive(:sentry_enabled?).and_return(true) + allow_any_instance_of(self.class).to receive(:rack_response) + end + + it 'does not report a MethodNotAllowed exception to Sentry' do + exception = Grape::Exceptions::MethodNotAllowed.new({ 'X-GitLab-Test' => '1' }) + allow(exception).to receive(:backtrace).and_return(caller) + + expect(Raven).not_to receive(:capture_exception).with(exception) + + handle_api_exception(exception) + end + + it 'does report RuntimeError to Sentry' do + exception = RuntimeError.new('test error') + allow(exception).to receive(:backtrace).and_return(caller) + + expect_any_instance_of(self.class).to receive(:sentry_context) + expect(Raven).to receive(:capture_exception).with(exception) + + handle_api_exception(exception) + end + end end diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb index 73c268c0d1e..5ad4fc4865a 100644 --- a/spec/requests/api/award_emoji_spec.rb +++ b/spec/requests/api/award_emoji_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' describe API::API, api: true do include ApiHelpers let(:user) { create(:user) } - let!(:project) { create(:project) } - let(:issue) { create(:issue, project: project, author: user) } + let!(: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) } @@ -39,6 +39,19 @@ describe API::API, api: true do 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 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) @@ -91,6 +104,20 @@ describe API::API, api: true do 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 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) @@ -115,6 +142,8 @@ describe API::API, api: true do end describe "POST /projects/:id/awardable/:awardable_id/award_emoji" do + let(:issue2) { create(:issue, project: project, author: user) } + context "on an issue" do it "creates a new award emoji" do post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'blowfish' @@ -136,6 +165,12 @@ describe API::API, api: true do expect(response).to have_http_status(401) end + it "returns a 404 error if the user authored issue" do + post api("/projects/#{project.id}/issues/#{issue2.id}/award_emoji", user), name: 'thumbsup' + + expect(response).to have_http_status(404) + end + it "normalizes +1 as thumbsup award" do post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: '+1' @@ -152,9 +187,23 @@ describe API::API, api: true do end end end + + context 'on a snippet' do + it 'creates a new award emoji' do + snippet = create(:project_snippet, :public, project: project) + + post 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 api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket' @@ -164,6 +213,12 @@ describe API::API, api: true do expect(json_response['user']['username']).to eq(user.username) end + it "it returns 404 error when user authored note" do + post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note2.id}/award_emoji", user), name: 'thumbsup' + + expect(response).to have_http_status(404) + end + it "normalizes +1 as thumbsup award" do post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: '+1' @@ -213,6 +268,19 @@ describe API::API, api: true do 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 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) + end + end end describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_emoji_id' do diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index 9444138f93d..3fd989dd7a6 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -243,7 +243,7 @@ describe API::API, api: true do end it "removes protected branch" do - project.protected_branches.create(name: branch_name) + create(:protected_branch, project: project, name: branch_name) delete 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') diff --git a/spec/requests/api/broadcast_messages_spec.rb b/spec/requests/api/broadcast_messages_spec.rb new file mode 100644 index 00000000000..7c9078b2864 --- /dev/null +++ b/spec/requests/api/broadcast_messages_spec.rb @@ -0,0 +1,180 @@ +require 'spec_helper' + +describe API::BroadcastMessages, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:admin) { create(:admin) } + + describe 'GET /broadcast_messages' do + it 'returns a 401 for anonymous users' do + get api('/broadcast_messages') + + expect(response).to have_http_status(401) + end + + it 'returns a 403 for users' do + get api('/broadcast_messages', user) + + expect(response).to have_http_status(403) + end + + it 'returns an Array of BroadcastMessages for admins' do + create(:broadcast_message) + + get api('/broadcast_messages', admin) + + expect(response).to have_http_status(200) + expect(json_response).to be_kind_of(Array) + expect(json_response.first.keys) + .to match_array(%w(id message starts_at ends_at color font active)) + end + end + + describe 'GET /broadcast_messages/:id' do + let!(:message) { create(:broadcast_message) } + + it 'returns a 401 for anonymous users' do + get api("/broadcast_messages/#{message.id}") + + expect(response).to have_http_status(401) + end + + it 'returns a 403 for users' do + get api("/broadcast_messages/#{message.id}", user) + + expect(response).to have_http_status(403) + end + + it 'returns the specified message for admins' do + get api("/broadcast_messages/#{message.id}", admin) + + expect(response).to have_http_status(200) + expect(json_response['id']).to eq message.id + expect(json_response.keys) + .to match_array(%w(id message starts_at ends_at color font active)) + end + end + + describe 'POST /broadcast_messages' do + it 'returns a 401 for anonymous users' do + post api('/broadcast_messages'), attributes_for(:broadcast_message) + + expect(response).to have_http_status(401) + end + + it 'returns a 403 for users' do + post api('/broadcast_messages', user), attributes_for(:broadcast_message) + + expect(response).to have_http_status(403) + end + + context 'as an admin' do + it 'requires the `message` parameter' do + attrs = attributes_for(:broadcast_message) + attrs.delete(:message) + + post api('/broadcast_messages', admin), attrs + + expect(response).to have_http_status(400) + expect(json_response['error']).to eq 'message is missing' + end + + it 'defines sane default start and end times' do + time = Time.zone.parse('2016-07-02 10:11:12') + travel_to(time) do + post api('/broadcast_messages', admin), message: 'Test message' + + expect(response).to have_http_status(201) + expect(json_response['starts_at']).to eq '2016-07-02T10:11:12.000Z' + expect(json_response['ends_at']).to eq '2016-07-02T11:11:12.000Z' + end + end + + it 'accepts a custom background and foreground color' do + attrs = attributes_for(:broadcast_message, color: '#000000', font: '#cecece') + + post api('/broadcast_messages', admin), attrs + + expect(response).to have_http_status(201) + expect(json_response['color']).to eq attrs[:color] + expect(json_response['font']).to eq attrs[:font] + end + end + end + + describe 'PUT /broadcast_messages/:id' do + let!(:message) { create(:broadcast_message) } + + it 'returns a 401 for anonymous users' do + put api("/broadcast_messages/#{message.id}"), + attributes_for(:broadcast_message) + + expect(response).to have_http_status(401) + end + + it 'returns a 403 for users' do + put api("/broadcast_messages/#{message.id}", user), + attributes_for(:broadcast_message) + + expect(response).to have_http_status(403) + end + + context 'as an admin' do + it 'accepts new background and foreground colors' do + attrs = { color: '#000000', font: '#cecece' } + + put api("/broadcast_messages/#{message.id}", admin), attrs + + expect(response).to have_http_status(200) + expect(json_response['color']).to eq attrs[:color] + expect(json_response['font']).to eq attrs[:font] + end + + it 'accepts new start and end times' do + time = Time.zone.parse('2016-07-02 10:11:12') + travel_to(time) do + attrs = { starts_at: Time.zone.now, ends_at: 3.hours.from_now } + + put api("/broadcast_messages/#{message.id}", admin), attrs + + expect(response).to have_http_status(200) + expect(json_response['starts_at']).to eq '2016-07-02T10:11:12.000Z' + expect(json_response['ends_at']).to eq '2016-07-02T13:11:12.000Z' + end + end + + it 'accepts a new message' do + attrs = { message: 'new message' } + + put api("/broadcast_messages/#{message.id}", admin), attrs + + expect(response).to have_http_status(200) + expect { message.reload }.to change { message.message }.to('new message') + end + end + end + + describe 'DELETE /broadcast_messages/:id' do + let!(:message) { create(:broadcast_message) } + + it 'returns a 401 for anonymous users' do + delete api("/broadcast_messages/#{message.id}"), + attributes_for(:broadcast_message) + + expect(response).to have_http_status(401) + end + + it 'returns a 403 for users' do + delete api("/broadcast_messages/#{message.id}", user), + attributes_for(:broadcast_message) + + expect(response).to have_http_status(403) + end + + it 'deletes the broadcast message for admins' do + expect { delete api("/broadcast_messages/#{message.id}", admin) } + .to change { BroadcastMessage.count }.by(-1) + end + end +end diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index 966d302dfd3..95c7bbf99c9 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -9,13 +9,15 @@ describe API::API, api: true do 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_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) } + 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/builds ' do let(:query) { '' } - before { get api("/projects/#{project.id}/builds?#{query}", api_user) } + before do + get api("/projects/#{project.id}/builds?#{query}", api_user) + end context 'authorized user' do it 'returns project builds' do @@ -28,6 +30,15 @@ describe API::API, api: true do 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' } @@ -89,6 +100,15 @@ describe API::API, api: true do expect(json_response).to be_an Array expect(json_response.size).to eq 2 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 end context 'when pipeline has no builds' do @@ -122,13 +142,24 @@ describe API::API, api: true do end describe 'GET /projects/:id/builds/:build_id' do - before { get api("/projects/#{project.id}/builds/#{build.id}", api_user) } + before do + get api("/projects/#{project.id}/builds/#{build.id}", api_user) + end context 'authorized user' do it 'returns specific build data' do 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 @@ -141,7 +172,9 @@ describe API::API, api: true do end describe 'GET /projects/:id/builds/:build_id/artifacts' do - before { get api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user) } + before do + get api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user) + end context 'build with artifacts' do let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } @@ -174,7 +207,11 @@ describe API::API, api: true do describe 'GET /projects/:id/artifacts/:ref_name/download?job=name' do let(:api_user) { reporter.user } - let(:build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } + let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } + + before do + build.success + end def path_for_ref(ref = pipeline.ref, job = build.name) api("/projects/#{project.id}/builds/artifacts/#{ref}/download?job=#{job}", api_user) @@ -288,7 +325,9 @@ describe API::API, api: true do end describe 'POST /projects/:id/builds/:build_id/cancel' do - before { post api("/projects/#{project.id}/builds/#{build.id}/cancel", api_user) } + before do + post api("/projects/#{project.id}/builds/#{build.id}/cancel", api_user) + end context 'authorized user' do context 'user with :update_build persmission' do @@ -319,7 +358,9 @@ describe API::API, api: true do describe 'POST /projects/:id/builds/:build_id/retry' do let(:build) { create(:ci_build, :canceled, pipeline: pipeline) } - before { post api("/projects/#{project.id}/builds/#{build.id}/retry", api_user) } + before do + post api("/projects/#{project.id}/builds/#{build.id}/retry", api_user) + end context 'authorized user' do context 'user with :update_build permission' do @@ -403,4 +444,27 @@ describe API::API, api: true do end end end + + describe 'POST /projects/:id/builds/:build_id/play' do + before do + post api("/projects/#{project.id}/builds/#{build.id}/play", user) + end + + context 'on an playable build' do + let(:build) { create(:ci_build, :manual, project: project, pipeline: pipeline) } + + it 'plays the build' 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 build' do + it 'returns a status code 400, Bad Request' do + expect(response).to have_http_status 400 + expect(response.body).to match("Unplayable Build") + end + end + end end diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb index 2d6093fec7a..7aa7e85a9e2 100644 --- a/spec/requests/api/commit_statuses_spec.rb +++ b/spec/requests/api/commit_statuses_spec.rb @@ -117,17 +117,36 @@ describe API::CommitStatuses, api: true do let(:post_url) { "/projects/#{project.id}/statuses/#{sha}" } context 'developer user' do - context 'only required parameters' do - before { post api(post_url, developer), state: 'success' } + %w[pending running success failed canceled].each do |status| + context "for #{status}" do + context 'uses only required parameters' do + it 'creates commit status' do + post api(post_url, developer), state: status + + expect(response).to have_http_status(201) + expect(json_response['sha']).to eq(commit.id) + expect(json_response['status']).to eq(status) + expect(json_response['name']).to eq('default') + expect(json_response['ref']).not_to be_empty + expect(json_response['target_url']).to be_nil + expect(json_response['description']).to be_nil + end + end + 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('default') - expect(json_response['ref']).to be_nil - expect(json_response['target_url']).to be_nil - expect(json_response['description']).to be_nil + context 'transitions status from pending' do + before do + post api(post_url, developer), state: 'pending' + end + + %w[running success failed canceled].each do |status| + it "to #{status}" do + expect { post api(post_url, developer), state: status }.not_to change { CommitStatus.count } + + expect(response).to have_http_status(201) + expect(json_response['status']).to eq(status) + end end end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 4379fcb3c1e..10f772c5b1a 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -89,16 +89,29 @@ describe API::API, api: true do it "returns nil for commit without CI" do get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) + expect(response).to have_http_status(200) expect(json_response['status']).to be_nil end it "returns status for CI" do - pipeline = project.ensure_pipeline(project.repository.commit.sha, 'master') + pipeline = project.ensure_pipeline('master', project.repository.commit.sha) + pipeline.update(status: 'success') + get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) + expect(response).to have_http_status(200) expect(json_response['status']).to eq(pipeline.status) end + + it "returns status for CI when pipeline is created" do + project.ensure_pipeline('master', project.repository.commit.sha) + + get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['status']).to eq("created") + end end context "unauthorized user" do diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb new file mode 100644 index 00000000000..8fa8c66db6c --- /dev/null +++ b/spec/requests/api/deployments_spec.rb @@ -0,0 +1,60 @@ +require 'spec_helper' + +describe API::API, 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 + + 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/environments_spec.rb b/spec/requests/api/environments_spec.rb index 05e57905343..1898b07835d 100644 --- a/spec/requests/api/environments_spec.rb +++ b/spec/requests/api/environments_spec.rb @@ -26,6 +26,7 @@ describe API::API, 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) end end diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index 2d1213df8a7..050d0dd082d 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -5,6 +5,21 @@ describe API::API, api: true do let(:user) { create(:user) } let!(:project) { create(:project, namespace: user.namespace ) } let(:file_path) { 'files/ruby/popen.rb' } + 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.") + # This seems to be what git does under the hood. For example, this commit: + # + # $ git commit --author='Foo Sr. <foo@example.com>' -m 'Where's my trailing period?' + # + # results in this: + # + # $ git show --pretty + # ... + # Author: Foo Sr <foo@example.com> + # ... + let(:author_name) { FFaker::Name.name.chomp("\.") } before { project.team << [user, :developer] } @@ -16,6 +31,7 @@ describe API::API, api: true do } get api("/projects/#{project.id}/repository/files", user), params + expect(response).to have_http_status(200) expect(json_response['file_path']).to eq(file_path) expect(json_response['file_name']).to eq('popen.rb') @@ -25,6 +41,7 @@ describe API::API, api: true do it "returns a 400 bad request if no params given" do get api("/projects/#{project.id}/repository/files", user) + expect(response).to have_http_status(400) end @@ -35,6 +52,7 @@ describe API::API, api: true do } get api("/projects/#{project.id}/repository/files", user), params + expect(response).to have_http_status(404) end end @@ -51,12 +69,17 @@ describe API::API, api: true do it "creates a new file in project repo" do post api("/projects/#{project.id}/repository/files", user), valid_params + expect(response).to have_http_status(201) expect(json_response['file_path']).to eq('newfile.rb') + last_commit = project.repository.commit.raw + expect(last_commit.author_email).to eq(user.email) + expect(last_commit.author_name).to eq(user.name) end it "returns a 400 bad request if no params given" do post api("/projects/#{project.id}/repository/files", user) + expect(response).to have_http_status(400) end @@ -65,8 +88,22 @@ describe API::API, api: true do and_return(false) post api("/projects/#{project.id}/repository/files", user), valid_params + expect(response).to have_http_status(400) end + + context "when specifying an author" do + it "creates a new file with the specified author" do + valid_params.merge!(author_email: author_email, author_name: author_name) + + post api("/projects/#{project.id}/repository/files", user), valid_params + + expect(response).to have_http_status(201) + last_commit = project.repository.commit.raw + expect(last_commit.author_email).to eq(author_email) + expect(last_commit.author_name).to eq(author_name) + end + end end describe "PUT /projects/:id/repository/files" do @@ -81,14 +118,32 @@ describe API::API, api: true do it "updates existing file in project repo" do put api("/projects/#{project.id}/repository/files", user), valid_params + expect(response).to have_http_status(200) expect(json_response['file_path']).to eq(file_path) + last_commit = project.repository.commit.raw + expect(last_commit.author_email).to eq(user.email) + expect(last_commit.author_name).to eq(user.name) end it "returns a 400 bad request if no params given" do put api("/projects/#{project.id}/repository/files", user) + expect(response).to have_http_status(400) end + + context "when specifying an author" do + it "updates a file with the specified author" do + valid_params.merge!(author_email: author_email, author_name: author_name, content: "New content") + + put api("/projects/#{project.id}/repository/files", user), valid_params + + expect(response).to have_http_status(200) + last_commit = project.repository.commit.raw + expect(last_commit.author_email).to eq(author_email) + expect(last_commit.author_name).to eq(author_name) + end + end end describe "DELETE /projects/:id/repository/files" do @@ -102,12 +157,17 @@ describe API::API, api: true do it "deletes existing file in project repo" do delete api("/projects/#{project.id}/repository/files", user), valid_params + expect(response).to have_http_status(200) expect(json_response['file_path']).to eq(file_path) + last_commit = project.repository.commit.raw + expect(last_commit.author_email).to eq(user.email) + expect(last_commit.author_name).to eq(user.name) end it "returns a 400 bad request if no params given" do delete api("/projects/#{project.id}/repository/files", user) + expect(response).to have_http_status(400) end @@ -115,8 +175,22 @@ describe API::API, api: true do allow_any_instance_of(Repository).to receive(:remove_file).and_return(false) delete api("/projects/#{project.id}/repository/files", user), valid_params + expect(response).to have_http_status(400) end + + context "when specifying an author" do + it "removes a file with the specified author" do + valid_params.merge!(author_email: author_email, author_name: author_name) + + delete api("/projects/#{project.id}/repository/files", user), valid_params + + expect(response).to have_http_status(200) + last_commit = project.repository.commit.raw + expect(last_commit.author_email).to eq(author_email) + expect(last_commit.author_name).to eq(author_name) + end + end end describe "POST /projects/:id/repository/files with binary file" do @@ -143,6 +217,7 @@ describe API::API, api: true do it "remains unchanged" do get api("/projects/#{project.id}/repository/files", user), get_params + expect(response).to have_http_status(200) expect(json_response['file_path']).to eq(file_path) expect(json_response['file_name']).to eq(file_path) diff --git a/spec/requests/api/fork_spec.rb b/spec/requests/api/fork_spec.rb index f802fcd2d2e..34f84f78952 100644 --- a/spec/requests/api/fork_spec.rb +++ b/spec/requests/api/fork_spec.rb @@ -6,6 +6,12 @@ describe API::API, api: true do let(:user2) { create(:user) } let(:user3) { create(:user) } let(:admin) { create(:admin) } + let(:group) { create(:group) } + let(:group2) do + group = create(:group, name: 'group2_name') + group.add_owner(user2) + group + end let(:project) do create(:project, creator_id: user.id, namespace: user.namespace) @@ -22,6 +28,7 @@ describe API::API, api: true do context 'when authenticated' do it 'forks if user has sufficient access to project' do post api("/projects/fork/#{project.id}", user2) + expect(response).to have_http_status(201) expect(json_response['name']).to eq(project.name) expect(json_response['path']).to eq(project.path) @@ -32,6 +39,7 @@ describe API::API, api: true do it 'forks if user is admin' do post api("/projects/fork/#{project.id}", admin) + expect(response).to have_http_status(201) expect(json_response['name']).to eq(project.name) expect(json_response['path']).to eq(project.path) @@ -42,12 +50,14 @@ describe API::API, api: true do it 'fails on missing project access for the project to fork' do post api("/projects/fork/#{project.id}", user3) + expect(response).to have_http_status(404) expect(json_response['message']).to eq('404 Project Not Found') end it 'fails if forked project exists in the user namespace' do post api("/projects/fork/#{project.id}", user) + expect(response).to have_http_status(409) expect(json_response['message']['name']).to eq(['has already been taken']) expect(json_response['message']['path']).to eq(['has already been taken']) @@ -55,14 +65,70 @@ describe API::API, api: true do it 'fails if project to fork from does not exist' do post api('/projects/fork/424242', user) + expect(response).to have_http_status(404) expect(json_response['message']).to eq('404 Project Not Found') end + + it 'forks with explicit own user namespace id' do + post api("/projects/fork/#{project.id}", user2), namespace: user2.namespace.id + + expect(response).to have_http_status(201) + expect(json_response['owner']['id']).to eq(user2.id) + end + + it 'forks with explicit own user name as namespace' do + post api("/projects/fork/#{project.id}", user2), namespace: user2.username + + expect(response).to have_http_status(201) + expect(json_response['owner']['id']).to eq(user2.id) + end + + it 'forks to another user when admin' do + post api("/projects/fork/#{project.id}", admin), namespace: user2.username + + expect(response).to have_http_status(201) + expect(json_response['owner']['id']).to eq(user2.id) + end + + it 'fails if trying to fork to another user when not admin' do + post api("/projects/fork/#{project.id}", user2), namespace: admin.namespace.id + + expect(response).to have_http_status(404) + end + + it 'fails if trying to fork to non-existent namespace' do + post api("/projects/fork/#{project.id}", user2), namespace: 42424242 + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Target Namespace Not Found') + end + + it 'forks to owned group' do + post api("/projects/fork/#{project.id}", user2), namespace: group2.name + + expect(response).to have_http_status(201) + expect(json_response['namespace']['name']).to eq(group2.name) + end + + it 'fails to fork to not owned group' do + post api("/projects/fork/#{project.id}", user2), namespace: group.name + + expect(response).to have_http_status(404) + end + + it 'forks to not owned group when admin' do + post api("/projects/fork/#{project.id}", admin), namespace: group.name + + expect(response).to have_http_status(201) + expect(json_response['namespace']['name']).to eq(group.name) + end end context 'when unauthenticated' do it 'returns authentication error' do post api("/projects/fork/#{project.id}") + expect(response).to have_http_status(401) expect(json_response['message']).to eq('401 Unauthorized') end diff --git a/spec/requests/api/group_members_spec.rb b/spec/requests/api/group_members_spec.rb deleted file mode 100644 index 8bd6a8062ae..00000000000 --- a/spec/requests/api/group_members_spec.rb +++ /dev/null @@ -1,199 +0,0 @@ -require 'spec_helper' - -describe API::API, api: true do - include ApiHelpers - - let(:owner) { create(:user) } - let(:reporter) { create(:user) } - let(:developer) { create(:user) } - let(:master) { create(:user) } - let(:guest) { create(:user) } - let(:stranger) { create(:user) } - - let!(:group_with_members) do - group = create(:group, :private) - group.add_users([reporter.id], GroupMember::REPORTER) - group.add_users([developer.id], GroupMember::DEVELOPER) - group.add_users([master.id], GroupMember::MASTER) - group.add_users([guest.id], GroupMember::GUEST) - group - end - - let!(:group_no_members) { create(:group) } - - before do - group_with_members.add_owner owner - group_no_members.add_owner owner - end - - describe "GET /groups/:id/members" do - context "when authenticated as user that is part or the group" do - it "each user: returns an array of members groups of group3" do - [owner, master, developer, reporter, guest].each do |user| - get api("/groups/#{group_with_members.id}/members", user) - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.size).to eq(5) - expect(json_response.find { |e| e['id'] == owner.id }['access_level']).to eq(GroupMember::OWNER) - expect(json_response.find { |e| e['id'] == reporter.id }['access_level']).to eq(GroupMember::REPORTER) - expect(json_response.find { |e| e['id'] == developer.id }['access_level']).to eq(GroupMember::DEVELOPER) - expect(json_response.find { |e| e['id'] == master.id }['access_level']).to eq(GroupMember::MASTER) - expect(json_response.find { |e| e['id'] == guest.id }['access_level']).to eq(GroupMember::GUEST) - end - end - - it 'users not part of the group should get access error' do - get api("/groups/#{group_with_members.id}/members", stranger) - - expect(response).to have_http_status(404) - end - end - end - - describe "POST /groups/:id/members" do - context "when not a member of the group" do - it "does not add guest as member of group_no_members when adding being done by person outside the group" do - post api("/groups/#{group_no_members.id}/members", reporter), user_id: guest.id, access_level: GroupMember::MASTER - expect(response).to have_http_status(403) - end - end - - context "when a member of the group" do - it "returns ok and add new member" do - new_user = create(:user) - - expect do - post api("/groups/#{group_no_members.id}/members", owner), user_id: new_user.id, access_level: GroupMember::MASTER - end.to change { group_no_members.members.count }.by(1) - - expect(response).to have_http_status(201) - expect(json_response['name']).to eq(new_user.name) - expect(json_response['access_level']).to eq(GroupMember::MASTER) - end - - it "does not allow guest to modify group members" do - new_user = create(:user) - - expect do - post api("/groups/#{group_with_members.id}/members", guest), user_id: new_user.id, access_level: GroupMember::MASTER - end.not_to change { group_with_members.members.count } - - expect(response).to have_http_status(403) - end - - it "returns error if member already exists" do - post api("/groups/#{group_with_members.id}/members", owner), user_id: master.id, access_level: GroupMember::MASTER - expect(response).to have_http_status(409) - end - - it "returns a 400 error when user id is not given" do - post api("/groups/#{group_no_members.id}/members", owner), access_level: GroupMember::MASTER - expect(response).to have_http_status(400) - end - - it "returns a 400 error when access level is not given" do - post api("/groups/#{group_no_members.id}/members", owner), user_id: master.id - expect(response).to have_http_status(400) - end - - it "returns a 422 error when access level is not known" do - post api("/groups/#{group_no_members.id}/members", owner), user_id: master.id, access_level: 1234 - expect(response).to have_http_status(422) - end - end - end - - describe 'PUT /groups/:id/members/:user_id' do - context 'when not a member of the group' do - it 'returns a 409 error if the user is not a group member' do - put( - api("/groups/#{group_no_members.id}/members/#{developer.id}", - owner), access_level: GroupMember::MASTER - ) - expect(response).to have_http_status(404) - end - end - - context 'when a member of the group' do - it 'returns ok and update member access level' do - put( - api("/groups/#{group_with_members.id}/members/#{reporter.id}", - owner), - access_level: GroupMember::MASTER - ) - - expect(response).to have_http_status(200) - - get api("/groups/#{group_with_members.id}/members", owner) - json_reporter = json_response.find do |e| - e['id'] == reporter.id - end - - expect(json_reporter['access_level']).to eq(GroupMember::MASTER) - end - - it 'does not allow guest to modify group members' do - put( - api("/groups/#{group_with_members.id}/members/#{developer.id}", - guest), - access_level: GroupMember::MASTER - ) - - expect(response).to have_http_status(403) - - get api("/groups/#{group_with_members.id}/members", owner) - json_developer = json_response.find do |e| - e['id'] == developer.id - end - - expect(json_developer['access_level']).to eq(GroupMember::DEVELOPER) - end - - it 'returns a 400 error when access level is not given' do - put( - api("/groups/#{group_with_members.id}/members/#{master.id}", owner) - ) - expect(response).to have_http_status(400) - end - - it 'returns a 422 error when access level is not known' do - put( - api("/groups/#{group_with_members.id}/members/#{master.id}", owner), - access_level: 1234 - ) - expect(response).to have_http_status(422) - end - end - end - - describe 'DELETE /groups/:id/members/:user_id' do - context 'when not a member of the group' do - it "does not delete guest's membership of group_with_members" do - random_user = create(:user) - delete api("/groups/#{group_with_members.id}/members/#{owner.id}", random_user) - - expect(response).to have_http_status(404) - end - end - - context "when a member of the group" do - it "deletes guest's membership of group" do - expect do - delete api("/groups/#{group_with_members.id}/members/#{guest.id}", owner) - end.to change { group_with_members.members.count }.by(-1) - - expect(response).to have_http_status(200) - end - - it "returns a 404 error when user id is not known" do - delete api("/groups/#{group_with_members.id}/members/1328", owner) - expect(response).to have_http_status(404) - end - - it "does not allow guest to modify group members" do - delete api("/groups/#{group_with_members.id}/members/#{master.id}", guest) - expect(response).to have_http_status(403) - end - end - end -end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 4860b23c2ed..1f68ef1af8f 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -120,10 +120,11 @@ describe API::API, api: true do context 'when authenticated as the group owner' do it 'updates the group' do - put api("/groups/#{group1.id}", user1), name: new_group_name + put 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 @@ -238,8 +239,14 @@ describe API::API, api: true do context "when authenticated as user with group permissions" do it "creates group" do - post api("/groups", user3), attributes_for(:group) + group = attributes_for(:group, { request_access_enabled: false }) + + post 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 "does not create group, duplicate" do diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index f6f85d6e95e..f0f590b0331 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -38,6 +38,105 @@ describe API::API, api: true do end end + describe 'GET /internal/two_factor_recovery_codes' do + it 'returns an error message when the key does not exist' do + post api('/internal/two_factor_recovery_codes'), + secret_token: secret_token, + key_id: 12345 + + expect(json_response['success']).to be_falsey + expect(json_response['message']).to eq('Could not find the given key') + end + + it 'returns an error message when the key is a deploy key' do + deploy_key = create(:deploy_key) + + post api('/internal/two_factor_recovery_codes'), + secret_token: secret_token, + key_id: deploy_key.id + + expect(json_response['success']).to be_falsey + expect(json_response['message']).to eq('Deploy keys cannot be used to retrieve recovery codes') + end + + it 'returns an error message when the user does not exist' do + key_without_user = create(:key, user: nil) + + post api('/internal/two_factor_recovery_codes'), + secret_token: secret_token, + key_id: key_without_user.id + + expect(json_response['success']).to be_falsey + expect(json_response['message']).to eq('Could not find a user for the given key') + expect(json_response['recovery_codes']).to be_nil + end + + context 'when two-factor is enabled' do + it 'returns new recovery codes when the user exists' do + allow_any_instance_of(User).to receive(:two_factor_enabled?).and_return(true) + allow_any_instance_of(User) + .to receive(:generate_otp_backup_codes!).and_return(%w(119135e5a3ebce8e 34bd7b74adbc8861)) + + post api('/internal/two_factor_recovery_codes'), + secret_token: secret_token, + key_id: key.id + + expect(json_response['success']).to be_truthy + expect(json_response['recovery_codes']).to match_array(%w(119135e5a3ebce8e 34bd7b74adbc8861)) + end + end + + context 'when two-factor is not enabled' do + it 'returns an error message' do + allow_any_instance_of(User).to receive(:two_factor_enabled?).and_return(false) + + post api('/internal/two_factor_recovery_codes'), + secret_token: secret_token, + key_id: key.id + + expect(json_response['success']).to be_falsey + expect(json_response['recovery_codes']).to be_nil + end + end + end + + describe "POST /internal/lfs_authenticate" do + before do + project.team << [user, :developer] + end + + context 'user key' do + it 'returns the correct information about the key' do + lfs_auth(key.id, project) + + expect(response).to have_http_status(200) + expect(json_response['username']).to eq(user.username) + expect(json_response['lfs_token']).to eq(Gitlab::LfsToken.new(key).token) + + expect(json_response['repository_http_path']).to eq(project.http_url_to_repo) + end + + it 'returns a 404 when the wrong key is provided' do + lfs_auth(nil, project) + + expect(response).to have_http_status(404) + end + end + + context 'deploy key' do + let(:key) { create(:deploy_key) } + + it 'returns the correct information about the key' do + lfs_auth(key.id, project) + + expect(response).to have_http_status(200) + expect(json_response['username']).to eq("lfs+deploy-key-#{key.id}") + expect(json_response['lfs_token']).to eq(Gitlab::LfsToken.new(key).token) + expect(json_response['repository_http_path']).to eq(project.http_url_to_repo) + end + end + end + describe "GET /internal/discover" do it do get(api("/internal/discover"), key_id: key.id, secret_token: secret_token) @@ -275,6 +374,24 @@ describe API::API, api: true do end end + describe 'GET /internal/merge_request_urls' do + let(:repo_name) { "#{project.namespace.name}/#{project.path}" } + let(:changes) { URI.escape("#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch") } + + 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 + expect(json_response).to match [{ + "branch_name" => "new_branch", + "url" => "http://localhost/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch", + "new_merge_request" => true + }] + end + end + def pull(key, project, protocol = 'ssh') post( api("/internal/allowed"), @@ -309,4 +426,13 @@ describe API::API, api: true do protocol: 'ssh' ) end + + def lfs_auth(key_id, project) + post( + api("/internal/lfs_authenticate"), + key_id: key_id, + secret_token: secret_token, + project: project.path_with_namespace + ) + end end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 3cd4e981fb2..f840778ae9b 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe API::API, api: true do include ApiHelpers + let(:user) { create(:user) } let(:user2) { create(:user) } let(:non_member) { create(:user) } @@ -16,21 +17,27 @@ describe API::API, api: true do assignee: user, project: project, state: :closed, - milestone: milestone + milestone: milestone, + created_at: generate(:issue_created_at), + updated_at: 3.hours.ago end let!(:confidential_issue) do create :issue, :confidential, project: project, author: author, - assignee: assignee + assignee: assignee, + created_at: generate(:issue_created_at), + updated_at: 2.hours.ago end let!(:issue) do create :issue, author: user, assignee: user, project: project, - milestone: milestone + milestone: milestone, + created_at: generate(:issue_created_at), + updated_at: 1.hour.ago end let!(:label) do create(:label, title: 'label', color: '#FFAABB', project: project) @@ -61,6 +68,7 @@ describe API::API, api: true do expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.first['title']).to eq(issue.title) + expect(json_response.last).to have_key('web_url') end it "adds pagination headers and keep query params" do @@ -133,6 +141,42 @@ describe API::API, api: true do expect(json_response).to be_an Array expect(json_response.length).to eq(0) end + + it 'sorts by created_at descending by default' do + get api('/issues', user) + response_dates = json_response.map { |issue| issue['created_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort.reverse) + end + + it 'sorts ascending when requested' do + get api('/issues?sort=asc', user) + response_dates = json_response.map { |issue| issue['created_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort) + end + + it 'sorts by updated_at descending when requested' do + get api('/issues?order_by=updated_at', user) + response_dates = json_response.map { |issue| issue['updated_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort.reverse) + end + + it 'sorts by updated_at ascending when requested' do + get api('/issues?order_by=updated_at&sort=asc', user) + response_dates = json_response.map { |issue| issue['updated_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort) + end end end @@ -145,21 +189,24 @@ describe API::API, api: true do assignee: user, project: group_project, state: :closed, - milestone: group_milestone + milestone: group_milestone, + updated_at: 3.hours.ago end let!(:group_confidential_issue) do create :issue, :confidential, project: group_project, author: author, - assignee: assignee + assignee: assignee, + updated_at: 2.hours.ago end let!(:group_issue) do create :issue, author: user, assignee: user, project: group_project, - milestone: group_milestone + milestone: group_milestone, + updated_at: 1.hour.ago end let!(:group_label) do create(:label, title: 'group_lbl', color: '#FFAABB', project: group_project) @@ -276,6 +323,42 @@ describe API::API, api: true do expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(group_closed_issue.id) end + + it 'sorts by created_at descending by default' do + get api(base_url, user) + response_dates = json_response.map { |issue| issue['created_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort.reverse) + end + + it 'sorts ascending when requested' do + get api("#{base_url}?sort=asc", user) + response_dates = json_response.map { |issue| issue['created_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort) + end + + it 'sorts by updated_at descending when requested' do + get api("#{base_url}?order_by=updated_at", user) + response_dates = json_response.map { |issue| issue['updated_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort.reverse) + end + + it 'sorts by updated_at ascending when requested' do + get api("#{base_url}?order_by=updated_at&sort=asc", user) + response_dates = json_response.map { |issue| issue['updated_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort) + end end describe "GET /projects/:id/issues" do @@ -384,6 +467,42 @@ describe API::API, api: true do expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(closed_issue.id) end + + it 'sorts by created_at descending by default' do + get api("#{base_url}/issues", user) + response_dates = json_response.map { |issue| issue['created_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort.reverse) + end + + it 'sorts ascending when requested' do + get api("#{base_url}/issues?sort=asc", user) + response_dates = json_response.map { |issue| issue['created_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort) + end + + it 'sorts by updated_at descending when requested' do + get api("#{base_url}/issues?order_by=updated_at", user) + response_dates = json_response.map { |issue| issue['updated_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort.reverse) + end + + it 'sorts by updated_at ascending when requested' do + get api("#{base_url}/issues?order_by=updated_at&sort=asc", user) + response_dates = json_response.map { |issue| issue['updated_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort) + end end describe "GET /projects/:id/issues/:issue_id" do @@ -403,6 +522,7 @@ describe API::API, api: true do expect(json_response['milestone']).to be_a Hash expect(json_response['assignee']).to be_a Hash expect(json_response['author']).to be_a Hash + expect(json_response['confidential']).to be_falsy end it "returns a project issue by id" do @@ -468,13 +588,63 @@ describe API::API, api: true do end describe "POST /projects/:id/issues" do - it "creates a new project issue" do + it 'creates a new project issue' do post api("/projects/#{project.id}/issues", user), title: 'new issue', labels: 'label, label2' + expect(response).to have_http_status(201) expect(json_response['title']).to eq('new issue') expect(json_response['description']).to be_nil expect(json_response['labels']).to eq(['label', 'label2']) + expect(json_response['confidential']).to be_falsy + end + + it 'creates a new confidential project issue' do + post api("/projects/#{project.id}/issues", user), + title: 'new issue', confidential: true + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq('new issue') + expect(json_response['confidential']).to be_truthy + end + + it 'creates a new confidential project issue with a different param' do + post api("/projects/#{project.id}/issues", user), + title: 'new issue', confidential: 'y' + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq('new issue') + expect(json_response['confidential']).to be_truthy + end + + it 'creates a public issue when confidential param is false' do + post api("/projects/#{project.id}/issues", user), + title: 'new issue', confidential: false + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq('new issue') + expect(json_response['confidential']).to be_falsy + end + + it 'creates a public issue when confidential param is invalid' do + post api("/projects/#{project.id}/issues", user), + title: 'new issue', confidential: 'foo' + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq('new issue') + expect(json_response['confidential']).to be_falsy + end + + it "sends notifications for subscribers of newly added labels" do + label = project.labels.first + label.toggle_subscription(user2) + + perform_enqueued_jobs do + post api("/projects/#{project.id}/issues", user), + title: 'new issue', labels: label.title + end + + should_email(user2) end it "returns a 400 bad request if title not given" do @@ -531,8 +701,8 @@ describe API::API, api: true do describe 'POST /projects/:id/issues with spam filtering' do before do - allow_any_instance_of(Gitlab::AkismetHelper).to receive(:check_for_spam?).and_return(true) - allow_any_instance_of(Gitlab::AkismetHelper).to receive(:is_spam?).and_return(true) + allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true) + allow_any_instance_of(AkismetService).to receive_messages(is_spam?: true) end let(:params) do @@ -554,7 +724,6 @@ describe API::API, api: true do expect(spam_logs[0].description).to eq('content here') expect(spam_logs[0].user).to eq(user) expect(spam_logs[0].noteable_type).to eq('Issue') - expect(spam_logs[0].project_id).to eq(project.id) end end @@ -619,6 +788,30 @@ describe API::API, api: true do expect(response).to have_http_status(200) expect(json_response['title']).to eq('updated title') end + + it 'sets an issue to confidential' do + put api("/projects/#{project.id}/issues/#{issue.id}", user), + confidential: true + + expect(response).to have_http_status(200) + expect(json_response['confidential']).to be_truthy + end + + it 'makes a confidential issue public' do + put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user), + confidential: false + + expect(response).to have_http_status(200) + expect(json_response['confidential']).to be_falsy + end + + it 'does not update a confidential issue with wrong confidential flag' do + put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user), + confidential: 'foo' + + expect(response).to have_http_status(200) + expect(json_response['confidential']).to be_truthy + end end end @@ -633,6 +826,18 @@ describe API::API, api: true do expect(json_response['labels']).to eq([label.title]) end + it "sends notifications for subscribers of newly added labels when issue is updated" do + label = create(:label, title: 'foo', color: '#FFAABB', project: project) + label.toggle_subscription(user2) + + perform_enqueued_jobs do + put api("/projects/#{project.id}/issues/#{issue.id}", user), + title: 'updated title', labels: label.title + end + + should_email(user2) + end + it 'removes all labels' do put api("/projects/#{project.id}/issues/#{issue.id}", user), labels: '' diff --git a/spec/requests/api/lint_spec.rb b/spec/requests/api/lint_spec.rb new file mode 100644 index 00000000000..391fc13a380 --- /dev/null +++ b/spec/requests/api/lint_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe API::Lint, api: true do + include ApiHelpers + + describe 'POST /ci/lint' do + context 'with valid .gitlab-ci.yaml content' do + let(:yaml_content) do + File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) + end + + it 'passes validation' do + post api('/ci/lint'), { content: yaml_content } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Hash + expect(json_response['status']).to eq('valid') + expect(json_response['errors']).to eq([]) + end + end + + context 'with an invalid .gitlab_ci.yml' do + it 'responds with errors about invalid syntax' do + post api('/ci/lint'), { content: 'invalid content' } + + expect(response).to have_http_status(200) + expect(json_response['status']).to eq('invalid') + expect(json_response['errors']).to eq(['Invalid configuration format']) + end + + it "responds with errors about invalid configuration" do + post api('/ci/lint'), { content: '{ image: "ruby:2.1", services: ["postgres"] }' } + + expect(response).to have_http_status(200) + expect(json_response['status']).to eq('invalid') + expect(json_response['errors']).to eq(['jobs config should contain at least one visible job']) + end + end + + context 'without the content parameter' do + it 'responds with validation error about missing content' do + post api('/ci/lint') + + expect(response).to have_http_status(400) + expect(json_response['error']).to eq('content is missing') + end + end + end +end diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb new file mode 100644 index 00000000000..92032f09b17 --- /dev/null +++ b/spec/requests/api/members_spec.rb @@ -0,0 +1,323 @@ +require 'spec_helper' + +describe API::Members, api: true do + include ApiHelpers + + let(:master) { create(:user) } + let(:developer) { create(:user) } + let(:access_requester) { create(:user) } + let(:stranger) { create(:user) } + + let(:project) do + project = create(:project, :public, creator_id: master.id, namespace: master.namespace) + project.team << [developer, :developer] + project.team << [master, :master] + project.request_access(access_requester) + project + end + + let!(:group) do + group = create(:group, :public) + group.add_developer(developer) + group.add_owner(master) + group.request_access(access_requester) + group + end + + shared_examples 'GET /:sources/:id/members' do |source_type| + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) { get api("/#{source_type.pluralize}/#{source.id}/members", stranger) } + end + + %i[master developer access_requester stranger].each do |type| + context "when authenticated as a #{type}" do + it 'returns 200' do + user = public_send(type) + get api("/#{source_type.pluralize}/#{source.id}/members", user) + + expect(response).to have_http_status(200) + expect(json_response.size).to eq(2) + expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id] + end + end + end + + it 'does not return invitees' do + create(:"#{source_type}_member", invite_token: '123', invite_email: 'test@abc.com', source: source, user: nil) + + get api("/#{source_type.pluralize}/#{source.id}/members", developer) + + expect(response).to have_http_status(200) + expect(json_response.size).to eq(2) + expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id] + end + + it 'finds members with query string' do + get api("/#{source_type.pluralize}/#{source.id}/members", developer), query: master.username + + expect(response).to have_http_status(200) + expect(json_response.count).to eq(1) + expect(json_response.first['username']).to eq(master.username) + end + end + end + + shared_examples 'GET /:sources/:id/members/:user_id' do |source_type| + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) { get api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) } + end + + context 'when authenticated as a non-member' do + %i[access_requester stranger].each do |type| + context "as a #{type}" do + it 'returns 200' do + user = public_send(type) + get api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user) + + expect(response).to have_http_status(200) + # User attributes + expect(json_response['id']).to eq(developer.id) + expect(json_response['name']).to eq(developer.name) + expect(json_response['username']).to eq(developer.username) + expect(json_response['state']).to eq(developer.state) + expect(json_response['avatar_url']).to eq(developer.avatar_url) + expect(json_response['web_url']).to eq(Gitlab::Routing.url_helpers.user_url(developer)) + + # Member attributes + expect(json_response['access_level']).to eq(Member::DEVELOPER) + end + end + end + end + end + end + + shared_examples 'POST /:sources/:id/members' do |source_type| + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) { post api("/#{source_type.pluralize}/#{source.id}/members", stranger) } + end + + context 'when authenticated as a non-member or member with insufficient rights' do + %i[access_requester stranger developer].each do |type| + context "as a #{type}" do + it 'returns 403' do + user = public_send(type) + post api("/#{source_type.pluralize}/#{source.id}/members", user) + + expect(response).to have_http_status(403) + end + end + end + end + + context 'when authenticated as a master/owner' do + context 'and new member is already a requester' do + it 'transforms the requester into a proper member' do + expect do + post api("/#{source_type.pluralize}/#{source.id}/members", master), + user_id: access_requester.id, access_level: Member::MASTER + + expect(response).to have_http_status(201) + end.to change { source.members.count }.by(1) + expect(source.requesters.count).to eq(0) + expect(json_response['id']).to eq(access_requester.id) + expect(json_response['access_level']).to eq(Member::MASTER) + end + end + + it 'creates a new member' do + expect do + post api("/#{source_type.pluralize}/#{source.id}/members", master), + user_id: stranger.id, access_level: Member::DEVELOPER, expires_at: '2016-08-05' + + expect(response).to have_http_status(201) + end.to change { source.members.count }.by(1) + expect(json_response['id']).to eq(stranger.id) + expect(json_response['access_level']).to eq(Member::DEVELOPER) + expect(json_response['expires_at']).to eq('2016-08-05') + end + end + + it "returns #{source_type == 'project' ? 201 : 409} if member already exists" do + post api("/#{source_type.pluralize}/#{source.id}/members", master), + user_id: master.id, access_level: Member::MASTER + + expect(response).to have_http_status(source_type == 'project' ? 201 : 409) + end + + it 'returns 400 when user_id is not given' do + post api("/#{source_type.pluralize}/#{source.id}/members", master), + access_level: Member::MASTER + + expect(response).to have_http_status(400) + end + + it 'returns 400 when access_level is not given' do + post api("/#{source_type.pluralize}/#{source.id}/members", master), + user_id: stranger.id + + expect(response).to have_http_status(400) + end + + it 'returns 422 when access_level is not valid' do + post api("/#{source_type.pluralize}/#{source.id}/members", master), + user_id: stranger.id, access_level: 1234 + + expect(response).to have_http_status(422) + end + end + end + + shared_examples 'PUT /:sources/:id/members/:user_id' do |source_type| + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) { put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) } + end + + context 'when authenticated as a non-member or member with insufficient rights' do + %i[access_requester stranger developer].each do |type| + context "as a #{type}" do + it 'returns 403' do + user = public_send(type) + put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user) + + expect(response).to have_http_status(403) + end + end + end + end + + context 'when authenticated as a master/owner' do + it 'updates the member' do + put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master), + access_level: Member::MASTER, expires_at: '2016-08-05' + + expect(response).to have_http_status(200) + expect(json_response['id']).to eq(developer.id) + expect(json_response['access_level']).to eq(Member::MASTER) + expect(json_response['expires_at']).to eq('2016-08-05') + end + end + + it 'returns 409 if member does not exist' do + put api("/#{source_type.pluralize}/#{source.id}/members/123", master), + access_level: Member::MASTER + + expect(response).to have_http_status(404) + end + + it 'returns 400 when access_level is not given' do + put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master) + + expect(response).to have_http_status(400) + end + + it 'returns 422 when access level is not valid' do + put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master), + access_level: 1234 + + expect(response).to have_http_status(422) + end + end + end + + shared_examples 'DELETE /:sources/:id/members/:user_id' do |source_type| + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) { delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) } + end + + context 'when authenticated as a non-member or member with insufficient rights' do + %i[access_requester stranger].each do |type| + context "as a #{type}" do + it 'returns 403' do + user = public_send(type) + delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user) + + expect(response).to have_http_status(403) + end + end + end + end + + context 'when authenticated as a member and deleting themself' do + it 'deletes the member' do + expect do + delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", developer) + + expect(response).to have_http_status(200) + end.to change { source.members.count }.by(-1) + end + end + + context 'when authenticated as a master/owner' do + context 'and member is a requester' do + it "returns #{source_type == 'project' ? 200 : 404}" do + expect do + delete api("/#{source_type.pluralize}/#{source.id}/members/#{access_requester.id}", master) + + expect(response).to have_http_status(source_type == 'project' ? 200 : 404) + end.not_to change { source.requesters.count } + end + end + + it 'deletes the member' do + expect do + delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master) + + expect(response).to have_http_status(200) + end.to change { source.members.count }.by(-1) + end + end + + it "returns #{source_type == 'project' ? 200 : 404} if member does not exist" do + delete api("/#{source_type.pluralize}/#{source.id}/members/123", master) + + expect(response).to have_http_status(source_type == 'project' ? 200 : 404) + end + end + end + + it_behaves_like 'GET /:sources/:id/members', 'project' do + let(:source) { project } + end + + it_behaves_like 'GET /:sources/:id/members', 'group' do + let(:source) { group } + end + + it_behaves_like 'GET /:sources/:id/members/:user_id', 'project' do + let(:source) { project } + end + + it_behaves_like 'GET /:sources/:id/members/:user_id', 'group' do + let(:source) { group } + end + + it_behaves_like 'POST /:sources/:id/members', 'project' do + let(:source) { project } + end + + it_behaves_like 'POST /:sources/:id/members', 'group' do + let(:source) { group } + end + + it_behaves_like 'PUT /:sources/:id/members/:user_id', 'project' do + let(:source) { project } + end + + it_behaves_like 'PUT /:sources/:id/members/:user_id', 'group' do + let(:source) { group } + end + + it_behaves_like 'DELETE /:sources/:id/members/:user_id', 'project' do + let(:source) { project } + end + + it_behaves_like 'DELETE /:sources/:id/members/:user_id', 'group' do + let(:source) { group } + end +end diff --git a/spec/requests/api/merge_request_diffs_spec.rb b/spec/requests/api/merge_request_diffs_spec.rb new file mode 100644 index 00000000000..8f1e5ac9891 --- /dev/null +++ b/spec/requests/api/merge_request_diffs_spec.rb @@ -0,0 +1,49 @@ +require "spec_helper" + +describe API::API, 'MergeRequestDiffs', api: true do + include ApiHelpers + + let!(:user) { create(:user) } + let!(:merge_request) { create(:merge_request, importing: true) } + let!(:project) { merge_request.target_project } + + before do + merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') + merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') + project.team << [user, :master] + end + + describe 'GET /projects/:id/merge_requests/:merge_request_id/versions' do + context 'valid merge request' do + before { get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions", user) } + let(:merge_request_diff) { merge_request.merge_request_diffs.first } + + it { expect(response.status).to eq 200 } + it { expect(json_response.size).to eq(merge_request.merge_request_diffs.size) } + it { expect(json_response.first['id']).to eq(merge_request_diff.id) } + it { expect(json_response.first['head_commit_sha']).to eq(merge_request_diff.head_commit_sha) } + end + + it 'returns a 404 when merge_request_id not found' do + get api("/projects/#{project.id}/merge_requests/999/versions", user) + expect(response).to have_http_status(404) + end + end + + describe 'GET /projects/:id/merge_requests/:merge_request_id/versions/:version_id' do + context 'valid merge request' do + before { get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/#{merge_request_diff.id}", user) } + let(:merge_request_diff) { merge_request.merge_request_diffs.first } + + it { expect(response.status).to eq 200 } + it { expect(json_response['id']).to eq(merge_request_diff.id) } + it { expect(json_response['head_commit_sha']).to eq(merge_request_diff.head_commit_sha) } + it { expect(json_response['diffs'].size).to eq(merge_request_diff.diffs.size) } + end + + it 'returns a 404 when merge_request_id not found' do + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/999", user) + expect(response).to have_http_status(404) + end + end +end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 617600d6173..a7930c59df9 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -9,7 +9,7 @@ describe API::API, api: true do let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) } let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) } - let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds) } + let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') } let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") } let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") } let(:milestone) { create(:milestone, title: '1.0.0', project: project) } @@ -33,6 +33,14 @@ describe API::API, api: true do expect(json_response).to be_an Array expect(json_response.length).to eq(3) expect(json_response.last['title']).to eq(merge_request.title) + expect(json_response.last).to have_key('web_url') + expect(json_response.last['sha']).to eq(merge_request.diff_head_sha) + expect(json_response.last['merge_commit_sha']).to be_nil + expect(json_response.last['merge_commit_sha']).to eq(merge_request.merge_commit_sha) + expect(json_response.first['title']).to eq(merge_request_merged.title) + expect(json_response.first['sha']).to eq(merge_request_merged.diff_head_sha) + expect(json_response.first['merge_commit_sha']).not_to be_nil + expect(json_response.first['merge_commit_sha']).to eq(merge_request_merged.merge_commit_sha) end it "returns an array of all merge_requests" do diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb index d6a0c656e74..dd192bea432 100644 --- a/spec/requests/api/milestones_spec.rb +++ b/spec/requests/api/milestones_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe API::API, api: true do include ApiHelpers let(:user) { create(:user) } - let!(:project) { create(:project, namespace: user.namespace ) } + let!(:project) { create(:empty_project, namespace: user.namespace ) } let!(:closed_milestone) { create(:closed_milestone, project: project) } let!(:milestone) { create(:milestone, project: project) } @@ -12,6 +12,7 @@ describe API::API, api: true do describe 'GET /projects/:id/milestones' do it 'returns project milestones' do get 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) @@ -19,6 +20,7 @@ describe API::API, api: true do it 'returns a 401 error if user not authenticated' do get api("/projects/#{project.id}/milestones") + expect(response).to have_http_status(401) end @@ -44,6 +46,7 @@ describe API::API, api: true do 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) @@ -60,11 +63,13 @@ describe API::API, api: true do it 'returns 401 error if user not authenticated' do get 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 api("/projects/#{project.id}/milestones/1234", user) + expect(response).to have_http_status(404) end end @@ -72,6 +77,7 @@ describe API::API, api: true do describe 'POST /projects/:id/milestones' do it 'creates a new project milestone' do post 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 @@ -80,6 +86,7 @@ describe API::API, api: true do it 'creates a new project milestone with description and due date' do post api("/projects/#{project.id}/milestones", user), title: 'new milestone', description: 'release', due_date: '2013-03-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') @@ -87,14 +94,31 @@ describe API::API, api: true do it 'returns a 400 error if title is missing' do post 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 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 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 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 @@ -102,6 +126,7 @@ describe API::API, api: true do it 'returns a 404 error if milestone id not found' do put api("/projects/#{project.id}/milestones/1234", user), title: 'updated title' + expect(response).to have_http_status(404) end end @@ -131,6 +156,7 @@ describe API::API, api: true do end it 'returns project issues for a particular milestone' do get 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) @@ -138,11 +164,12 @@ describe API::API, api: true do it 'returns a 401 error if user not authenticated' do get api("/projects/#{project.id}/milestones/#{milestone.id}/issues") + expect(response).to have_http_status(401) end describe 'confidential issues' do - let(:public_project) { create(:project, :public) } + 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) } diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index 737fa14cbb0..063a8706e76 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -25,7 +25,7 @@ describe API::API, api: true do let!(:cross_reference_note) do create :note, noteable: ext_issue, project: ext_proj, - note: "mentioned in issue #{private_issue.to_reference(ext_proj)}", + note: "Mentioned in issue #{private_issue.to_reference(ext_proj)}", system: true end @@ -220,6 +220,15 @@ describe API::API, api: true do expect(Time.parse(json_response['created_at'])).to be_within(1.second).of(creation_time) end end + + context 'when the user is posting an award emoji' do + it 'returns an award emoji' do + post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: ':+1:' + + expect(response).to have_http_status(201) + expect(json_response['awardable_id']).to eq issue.id + end + end end context "when noteable is a Snippet" do diff --git a/spec/requests/api/notification_settings_spec.rb b/spec/requests/api/notification_settings_spec.rb new file mode 100644 index 00000000000..e6d8a5ee954 --- /dev/null +++ b/spec/requests/api/notification_settings_spec.rb @@ -0,0 +1,89 @@ +require 'spec_helper' + +describe API::API, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let!(:group) { create(:group) } + let!(:project) { create(:project, :public, creator_id: user.id, namespace: group) } + + describe "GET /notification_settings" do + it "returns global notification settings for the current user" do + get api("/notification_settings", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_a Hash + expect(json_response['notification_email']).to eq(user.notification_email) + expect(json_response['level']).to eq(user.global_notification_setting.level) + end + end + + describe "PUT /notification_settings" do + let(:email) { create(:email, user: user) } + + it "updates global notification settings for the current user" do + put api("/notification_settings", user), { level: 'watch', notification_email: email.email } + + expect(response).to have_http_status(200) + expect(json_response['notification_email']).to eq(email.email) + expect(user.reload.notification_email).to eq(email.email) + expect(json_response['level']).to eq(user.reload.global_notification_setting.level) + end + end + + describe "PUT /notification_settings" do + it "fails on non-user email address" do + put api("/notification_settings", user), { notification_email: 'invalid@example.com' } + + expect(response).to have_http_status(400) + end + end + + describe "GET /groups/:id/notification_settings" do + it "returns group level notification settings for the current user" do + get api("/groups/#{group.id}/notification_settings", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_a Hash + expect(json_response['level']).to eq(user.notification_settings_for(group).level) + end + end + + describe "PUT /groups/:id/notification_settings" do + it "updates group level notification settings for the current user" do + put api("/groups/#{group.id}/notification_settings", user), { level: 'watch' } + + expect(response).to have_http_status(200) + expect(json_response['level']).to eq(user.reload.notification_settings_for(group).level) + end + end + + describe "GET /projects/:id/notification_settings" do + it "returns project level notification settings for the current user" do + get api("/projects/#{project.id}/notification_settings", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_a Hash + expect(json_response['level']).to eq(user.notification_settings_for(project).level) + end + end + + describe "PUT /projects/:id/notification_settings" do + it "updates project level notification settings for the current user" do + put api("/projects/#{project.id}/notification_settings", user), { level: 'custom', new_note: true } + + expect(response).to have_http_status(200) + expect(json_response['level']).to eq(user.reload.notification_settings_for(project).level) + expect(json_response['events']['new_note']).to eq(true) + expect(json_response['events']['new_issue']).to eq(false) + end + end + + describe "PUT /projects/:id/notification_settings" do + it "fails on invalid level" do + put api("/projects/#{project.id}/notification_settings", user), { level: 'invalid' } + + expect(response).to have_http_status(400) + end + end +end diff --git a/spec/requests/api/oauth_tokens_spec.rb b/spec/requests/api/oauth_tokens_spec.rb new file mode 100644 index 00000000000..7e2cc50e591 --- /dev/null +++ b/spec/requests/api/oauth_tokens_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe API::API, api: true do + include ApiHelpers + + context 'Resource Owner Password Credentials' do + def request_oauth_token(user) + post '/oauth/token', username: user.username, password: user.password, grant_type: 'password' + end + + context 'when user has 2FA enabled' do + it 'does not create an access token' do + user = create(:user, :two_factor) + + request_oauth_token(user) + + expect(response).to have_http_status(401) + expect(json_response['error']).to eq('invalid_grant') + end + end + + context 'when user does not have 2FA enabled' do + it 'creates an access token' do + user = create(:user) + + request_oauth_token(user) + + expect(response).to have_http_status(200) + expect(json_response['access_token']).not_to be_nil + end + end + end +end diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb new file mode 100644 index 00000000000..7011bdc9ec0 --- /dev/null +++ b/spec/requests/api/pipelines_spec.rb @@ -0,0 +1,133 @@ +require 'spec_helper' + +describe API::API, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:non_member) { create(:user) } + let(:project) { create(:project, creator_id: user.id) } + + let!(:pipeline) do + create(:ci_empty_pipeline, project: project, sha: project.commit.id, + ref: project.default_branch) + end + + before { project.team << [user, :master] } + + describe 'GET /projects/:id/pipelines ' do + it_behaves_like 'a paginated resources' do + let(:request) { get api("/projects/#{project.id}/pipelines", user) } + end + + context 'authorized user' do + it 'returns project pipelines' do + get api("/projects/#{project.id}/pipelines", user) + + expect(response).to have_http_status(200) + expect(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 + end + end + + context 'unauthorized user' do + it 'does not return project pipelines' do + get 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 'GET /projects/:id/pipelines/:pipeline_id' do + context 'authorized user' do + it 'returns project pipelines' do + get 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 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 + end + + context 'unauthorized user' do + it 'should not return a project pipeline' do + get 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 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 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 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 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/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb index 34fac297923..765dc8a8f66 100644 --- a/spec/requests/api/project_hooks_spec.rb +++ b/spec/requests/api/project_hooks_spec.rb @@ -7,9 +7,9 @@ describe API::API, 'ProjectHooks', api: true do let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } let!(:hook) do create(:project_hook, - project: project, url: "http://example.com", - push_events: true, merge_requests_events: true, tag_push_events: true, - issues_events: true, note_events: true, build_events: true, + :all_events_enabled, + project: project, + url: 'http://example.com', enable_ssl_verification: true) end @@ -33,6 +33,8 @@ describe API::API, 'ProjectHooks', api: true do 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 @@ -56,6 +58,9 @@ describe API::API, '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['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 @@ -91,6 +96,8 @@ describe API::API, 'ProjectHooks', api: true do 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(false) expect(json_response['enable_ssl_verification']).to eq(true) end @@ -116,6 +123,9 @@ describe API::API, '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['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 diff --git a/spec/requests/api/project_members_spec.rb b/spec/requests/api/project_members_spec.rb deleted file mode 100644 index 13cc0d81ac8..00000000000 --- a/spec/requests/api/project_members_spec.rb +++ /dev/null @@ -1,166 +0,0 @@ -require 'spec_helper' - -describe API::API, api: true do - include ApiHelpers - let(:user) { create(:user) } - let(:user2) { create(:user) } - let(:user3) { create(:user) } - let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } - let(:project_member) { create(:project_member, :master, user: user, project: project) } - let(:project_member2) { create(:project_member, :developer, user: user3, project: project) } - - describe "GET /projects/:id/members" do - before { project_member } - before { project_member2 } - - it "returns project team members" do - get api("/projects/#{project.id}/members", user) - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.count).to eq(2) - expect(json_response.map { |u| u['username'] }).to include user.username - end - - it "finds team members with query string" do - get api("/projects/#{project.id}/members", user), query: user.username - 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['username']).to eq(user.username) - end - - it "returns a 404 error if id not found" do - get api("/projects/9999/members", user) - expect(response).to have_http_status(404) - end - end - - describe "GET /projects/:id/members/:user_id" do - before { project_member } - - it "returns project team member" do - get api("/projects/#{project.id}/members/#{user.id}", user) - expect(response).to have_http_status(200) - expect(json_response['username']).to eq(user.username) - expect(json_response['access_level']).to eq(ProjectMember::MASTER) - end - - it "returns a 404 error if user id not found" do - get api("/projects/#{project.id}/members/1234", user) - expect(response).to have_http_status(404) - end - end - - describe "POST /projects/:id/members" do - it "adds user to project team" do - expect do - post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: ProjectMember::DEVELOPER - end.to change { ProjectMember.count }.by(1) - - expect(response).to have_http_status(201) - expect(json_response['username']).to eq(user2.username) - expect(json_response['access_level']).to eq(ProjectMember::DEVELOPER) - end - - it "returns a 201 status if user is already project member" do - post api("/projects/#{project.id}/members", user), - user_id: user2.id, - access_level: ProjectMember::DEVELOPER - expect do - post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: ProjectMember::DEVELOPER - end.not_to change { ProjectMember.count } - - expect(response).to have_http_status(201) - expect(json_response['username']).to eq(user2.username) - expect(json_response['access_level']).to eq(ProjectMember::DEVELOPER) - end - - it "returns a 400 error when user id is not given" do - post api("/projects/#{project.id}/members", user), access_level: ProjectMember::MASTER - expect(response).to have_http_status(400) - end - - it "returns a 400 error when access level is not given" do - post api("/projects/#{project.id}/members", user), user_id: user2.id - expect(response).to have_http_status(400) - end - - it "returns a 422 error when access level is not known" do - post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: 1234 - expect(response).to have_http_status(422) - end - end - - describe "PUT /projects/:id/members/:user_id" do - before { project_member2 } - - it "updates project team member" do - put api("/projects/#{project.id}/members/#{user3.id}", user), access_level: ProjectMember::MASTER - expect(response).to have_http_status(200) - expect(json_response['username']).to eq(user3.username) - expect(json_response['access_level']).to eq(ProjectMember::MASTER) - end - - it "returns a 404 error if user_id is not found" do - put api("/projects/#{project.id}/members/1234", user), access_level: ProjectMember::MASTER - expect(response).to have_http_status(404) - end - - it "returns a 400 error when access level is not given" do - put api("/projects/#{project.id}/members/#{user3.id}", user) - expect(response).to have_http_status(400) - end - - it "returns a 422 error when access level is not known" do - put api("/projects/#{project.id}/members/#{user3.id}", user), access_level: 123 - expect(response).to have_http_status(422) - end - end - - describe "DELETE /projects/:id/members/:user_id" do - before do - project_member - project_member2 - end - - it "removes user from project team" do - expect do - delete api("/projects/#{project.id}/members/#{user3.id}", user) - end.to change { ProjectMember.count }.by(-1) - end - - it "returns 200 if team member is not part of a project" do - delete api("/projects/#{project.id}/members/#{user3.id}", user) - expect do - delete api("/projects/#{project.id}/members/#{user3.id}", user) - end.not_to change { ProjectMember.count } - expect(response).to have_http_status(200) - end - - it "returns 200 if team member already removed" do - delete api("/projects/#{project.id}/members/#{user3.id}", user) - delete api("/projects/#{project.id}/members/#{user3.id}", user) - expect(response).to have_http_status(200) - end - - it "returns 200 OK when the user was not member" do - expect do - delete api("/projects/#{project.id}/members/1000000", user) - end.to change { ProjectMember.count }.by(0) - expect(response).to have_http_status(200) - expect(json_response['id']).to eq(1000000) - expect(json_response['message']).to eq('Access revoked') - end - - context 'when the user is not an admin or owner' do - it 'can leave the project' do - expect do - delete api("/projects/#{project.id}/members/#{user3.id}", user3) - end.to change { ProjectMember.count }.by(-1) - - expect(response).to have_http_status(200) - expect(json_response['id']).to eq(project_member2.id) - end - end - end -end diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index 42757ff21b0..01148f0a05e 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -30,6 +30,7 @@ describe API::API, api: true do expect(response).to have_http_status(200) expect(json_response.size).to eq(3) expect(json_response.map{ |snippet| snippet['id']} ).to include(public_snippet.id, internal_snippet.id, private_snippet.id) + expect(json_response.last).to have_key('web_url') end it 'hides private snippets from regular user' do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 4742b3d0e37..4a0d727faea 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -73,7 +73,7 @@ describe API::API, api: true do end it 'does not include open_issues_count' do - project.update_attributes( { issues_enabled: false } ) + project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) get api('/projects', user) expect(response.status).to eq 200 @@ -224,14 +224,23 @@ describe API::API, api: true do description: FFaker::Lorem.sentence, issues_enabled: false, merge_requests_enabled: false, - wiki_enabled: false + wiki_enabled: false, + only_allow_merge_if_build_succeeds: false, + request_access_enabled: true }) post api('/projects', user), project project.each_pair do |k, v| + next if %i{ issues_enabled merge_requests_enabled wiki_enabled }.include?(k) expect(json_response[k.to_s]).to eq(v) end + + # Check feature permissions attributes + project = Project.find_by_path(project[:path]) + expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED) + expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::DISABLED) + expect(project.project_feature.wiki_access_level).to eq(ProjectFeature::DISABLED) end it 'sets a project as public' do @@ -276,6 +285,18 @@ describe API::API, api: true do expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::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 }) + post api('/projects', user), project + 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 + project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true }) + post api('/projects', user), project + expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy + end + context 'when a visibility level is restricted' do before do @project = attributes_for(:project, { public: true }) @@ -332,7 +353,8 @@ describe API::API, api: true do description: FFaker::Lorem.sentence, issues_enabled: false, merge_requests_enabled: false, - wiki_enabled: false + wiki_enabled: false, + request_access_enabled: true }) post api("/projects/user/#{user.id}", admin), project @@ -384,6 +406,18 @@ describe API::API, api: true do expect(json_response['public']).to be_falsey expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::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 }) + post api("/projects/user/#{user.id}", admin), project + 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 + project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true }) + post api("/projects/user/#{user.id}", admin), project + expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy + end end describe "POST /projects/:id/uploads" do @@ -444,6 +478,7 @@ describe API::API, 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) end it 'returns a project by path name' do @@ -726,13 +761,16 @@ describe API::API, api: true do let(:group) { create(:group) } it "shares project with group" do + expires_at = 10.days.from_now.to_date + expect do - post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER + post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER, expires_at: expires_at end.to change { ProjectGroupLink.count }.by(1) expect(response.status).to eq 201 - expect(json_response['group_id']).to eq group.id - expect(json_response['group_access']).to eq Gitlab::Access::DEVELOPER + expect(json_response['group_id']).to eq(group.id) + expect(json_response['group_access']).to eq(Gitlab::Access::DEVELOPER) + expect(json_response['expires_at']).to eq(expires_at.to_s) end it "returns a 400 error when group id is not given" do @@ -854,6 +892,15 @@ describe API::API, api: true do expect(json_response['message']['name']).to eq(['has already been taken']) end + it 'updates request_access_enabled' do + project_param = { request_access_enabled: false } + + put api("/projects/#{project.id}", user), project_param + + expect(response).to have_http_status(200) + expect(json_response['request_access_enabled']).to eq(false) + end + it 'updates path & name to existing path & name in different namespace' do project_param = { path: project4.path, name: project4.name } put api("/projects/#{project3.id}", user), project_param @@ -915,7 +962,8 @@ describe API::API, api: true do wiki_enabled: true, snippets_enabled: true, merge_requests_enabled: true, - description: 'new description' } + description: 'new description', + request_access_enabled: true } put api("/projects/#{project.id}", user3), project_param expect(response).to have_http_status(403) end diff --git a/spec/requests/api/session_spec.rb b/spec/requests/api/session_spec.rb index 519e7ce12ad..acad1365ace 100644 --- a/spec/requests/api/session_spec.rb +++ b/spec/requests/api/session_spec.rb @@ -17,6 +17,17 @@ describe API::API, api: true do expect(json_response['can_create_project']).to eq(user.can_create_project?) expect(json_response['can_create_group']).to eq(user.can_create_group?) end + + context 'with 2FA enabled' do + it 'rejects sign in attempts' do + user = create(:user, :two_factor) + + post api('/session'), email: user.email, password: user.password + + expect(response).to have_http_status(401) + expect(response.body).to include('You have 2FA enabled.') + end + end end context 'when email has case-typo and password is valid' do diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index 54d096e8b7f..f4903d8e0be 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -14,22 +14,38 @@ describe API::API, 'Settings', api: true do 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 end end describe "PUT /application/settings" do - before do - storages = { 'custom' => 'tmp/tests/custom_repositories' } - allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) + 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 api("/application/settings", admin), + default_projects_limit: 3, signin_enabled: false, repository_storage: 'custom', koding_enabled: true, koding_url: 'http://koding.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['koding_enabled']).to be_truthy + expect(json_response['koding_url']).to eq('http://koding.example.com') + end end - it "updates application settings" do - put api("/application/settings", admin), - default_projects_limit: 3, signin_enabled: false, repository_storage: 'custom' - 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') + context "missing koding_url value when koding_enabled is true" do + it "returns a blank parameter error message" do + put api("/application/settings", admin), koding_enabled: true + + expect(response).to have_http_status(400) + expect(json_response['message']).to have_key('koding_url') + expect(json_response['message']['koding_url']).to include "can't be blank" + end end end end diff --git a/spec/requests/api/templates_spec.rb b/spec/requests/api/templates_spec.rb index 68d0f41b489..5bd5b861792 100644 --- a/spec/requests/api/templates_spec.rb +++ b/spec/requests/api/templates_spec.rb @@ -3,50 +3,53 @@ require 'spec_helper' describe API::Templates, api: true do include ApiHelpers - describe 'the Template Entity' do - before { get api('/gitignores/Ruby') } + context 'global templates' do + describe 'the Template Entity' do + before { get api('/gitignores/Ruby') } - it { expect(json_response['name']).to eq('Ruby') } - it { expect(json_response['content']).to include('*.gem') } - end + it { expect(json_response['name']).to eq('Ruby') } + it { expect(json_response['content']).to include('*.gem') } + end - describe 'the TemplateList Entity' do - before { get api('/gitignores') } + describe 'the TemplateList Entity' do + before { get api('/gitignores') } - it { expect(json_response.first['name']).not_to be_nil } - it { expect(json_response.first['content']).to be_nil } - end + it { expect(json_response.first['name']).not_to be_nil } + it { expect(json_response.first['content']).to be_nil } + end - context 'requesting gitignores' do - describe 'GET /gitignores' do - it 'returns a list of available gitignore templates' do - get api('/gitignores') + context 'requesting gitignores' do + describe 'GET /gitignores' do + it 'returns a list of available gitignore templates' do + get api('/gitignores') - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.size).to be > 15 + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.size).to be > 15 + end end end - end - context 'requesting gitlab-ci-ymls' do - describe 'GET /gitlab_ci_ymls' do - it 'returns a list of available gitlab_ci_ymls' do - get api('/gitlab_ci_ymls') + context 'requesting gitlab-ci-ymls' do + describe 'GET /gitlab_ci_ymls' do + it 'returns a list of available gitlab_ci_ymls' do + get api('/gitlab_ci_ymls') - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['name']).not_to be_nil + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).not_to be_nil + end end end - end - describe 'GET /gitlab_ci_ymls/Ruby' do - it 'adds a disclaimer on the top' do - get api('/gitlab_ci_ymls/Ruby') + describe 'GET /gitlab_ci_ymls/Ruby' do + it 'adds a disclaimer on the top' do + get api('/gitlab_ci_ymls/Ruby') - expect(response).to have_http_status(200) - expect(json_response['content']).to start_with("# This file is a template,") + expect(response).to have_http_status(200) + expect(json_response['name']).not_to be_nil + expect(json_response['content']).to start_with("# This file is a template,") + end end end end diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb index 3ccd0af652f..887a2ba5b84 100644 --- a/spec/requests/api/todos_spec.rb +++ b/spec/requests/api/todos_spec.rb @@ -117,6 +117,12 @@ describe API::Todos, api: true do expect(response.status).to eq(200) expect(pending_1.reload).to be_done end + + it 'updates todos cache' do + expect_any_instance_of(User).to receive(:update_todos_count_cache).and_call_original + + delete api("/todos/#{pending_1.id}", john_doe) + end end end @@ -139,6 +145,12 @@ describe API::Todos, api: true do expect(pending_2.reload).to be_done expect(pending_3.reload).to be_done end + + it 'updates todos cache' do + expect_any_instance_of(User).to receive(:update_todos_count_cache).and_call_original + + delete api("/todos", john_doe) + end end end diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb index 5702682fc7d..82bba1ce8a4 100644 --- a/spec/requests/api/triggers_spec.rb +++ b/spec/requests/api/triggers_spec.rb @@ -50,7 +50,8 @@ describe API::API do post api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'master') expect(response).to have_http_status(201) pipeline.builds.reload - expect(pipeline.builds.size).to eq(2) + 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 diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index e0e041b4e15..f4ea3bebb4c 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -62,6 +62,7 @@ describe API::API, api: true do expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.first.keys).to include 'email' + expect(json_response.first.keys).to include 'organization' expect(json_response.first.keys).to include 'identities' expect(json_response.first.keys).to include 'can_create_project' expect(json_response.first.keys).to include 'two_factor_enabled' @@ -265,6 +266,14 @@ describe API::API, api: true do expect(user.reload.bio).to eq('new test bio') end + it "updates user with organization" do + put api("/users/#{user.id}", admin), { organization: 'GitLab' } + + expect(response).to have_http_status(200) + expect(json_response['organization']).to eq('GitLab') + expect(user.reload.organization).to eq('GitLab') + end + it 'updates user with his own email' do put api("/users/#{user.id}", admin), email: user.email expect(response).to have_http_status(200) @@ -564,12 +573,14 @@ describe API::API, api: true do end describe "DELETE /users/:id" do + let!(:namespace) { user.namespace } before { admin } it "deletes user" do delete api("/users/#{user.id}", admin) expect(response).to have_http_status(200) 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 @@ -603,6 +614,7 @@ describe API::API, api: true do expect(json_response['can_create_project']).to eq(user.can_create_project?) expect(json_response['can_create_group']).to eq(user.can_create_group?) expect(json_response['projects_limit']).to eq(user.projects_limit) + expect(json_response['private_token']).to be_blank end it "returns 401 error if user is unauthenticated" do |