diff options
Diffstat (limited to 'spec/requests/api')
-rw-r--r-- | spec/requests/api/builds_spec.rb | 1 | ||||
-rw-r--r-- | spec/requests/api/issues_spec.rb | 19 | ||||
-rw-r--r-- | spec/requests/api/merge_requests_spec.rb | 28 | ||||
-rw-r--r-- | spec/requests/api/projects_spec.rb | 46 | ||||
-rw-r--r-- | spec/requests/api/v3/issues_spec.rb | 1259 | ||||
-rw-r--r-- | spec/requests/api/v3/merge_requests_spec.rb | 726 | ||||
-rw-r--r-- | spec/requests/api/v3/projects_spec.rb | 1424 |
7 files changed, 3421 insertions, 82 deletions
diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index f197fadebab..834c4e52693 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -188,6 +188,7 @@ describe API::Builds, api: true do it 'returns specific job artifacts' do expect(response).to have_http_status(200) expect(response.headers).to include(download_headers) + expect(response.body).to match_file(build.artifacts_file.file.file) end end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 62f1b8d7ca2..cca00df9591 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -425,7 +425,7 @@ describe API::Issues, api: true do end it 'returns no issues when user has access to project but not issues' do - restricted_project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) + restricted_project = create(:empty_project, :public, :issues_private) create(:issue, project: restricted_project) get api("/projects/#{restricted_project.id}/issues", non_member) @@ -612,23 +612,6 @@ describe API::Issues, api: true do expect(json_response['iid']).to eq(issue.iid) end - it 'returns a project issue by iid' do - get api("/projects/#{project.id}/issues?iid=#{issue.iid}", user) - - expect(response.status).to eq 200 - expect(json_response.length).to eq 1 - expect(json_response.first['title']).to eq issue.title - expect(json_response.first['id']).to eq issue.id - expect(json_response.first['iid']).to eq issue.iid - end - - it 'returns an empty array for an unknown project issue iid' do - get api("/projects/#{project.id}/issues?iid=#{issue.iid + 10}", user) - - expect(response.status).to eq 200 - expect(json_response.length).to eq 0 - end - it "returns 404 if issue id not found" do get api("/projects/#{project.id}/issues/54321", user) expect(response).to have_http_status(404) diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 21a2c583aa8..ff10e79e417 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -73,6 +73,16 @@ describe API::MergeRequests, api: true do expect(json_response.first['title']).to eq(merge_request_merged.title) end + it 'returns merge_request by "iids" array' do + get api("/projects/#{project.id}/merge_requests", user), iids: [merge_request.iid, merge_request_closed.iid] + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.first['title']).to eq merge_request_closed.title + expect(json_response.first['id']).to eq merge_request_closed.id + end + context "with ordering" do before do @mr_later = mr_with_later_created_and_updated_at_time @@ -159,24 +169,6 @@ describe API::MergeRequests, api: true do expect(json_response['force_close_merge_request']).to be_falsy end - it 'returns merge_request by iid' do - url = "/projects/#{project.id}/merge_requests?iid=#{merge_request.iid}" - get api(url, user) - expect(response.status).to eq 200 - expect(json_response.first['title']).to eq merge_request.title - expect(json_response.first['id']).to eq merge_request.id - end - - it 'returns merge_request by iid array' do - get api("/projects/#{project.id}/merge_requests", user), iid: [merge_request.iid, merge_request_closed.iid] - - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) - expect(json_response.first['title']).to eq merge_request_closed.title - expect(json_response.first['id']).to eq merge_request_closed.id - end - it "returns a 404 error if merge_request_id not found" do get api("/projects/#{project.id}/merge_requests/999", user) expect(response).to have_http_status(404) diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 753dde0ca3a..225e2e005df 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1085,52 +1085,6 @@ describe API::Projects, api: true do end end - describe 'GET /projects/search/:query' do - let!(:query) { 'query'} - let!(:search) { create(:empty_project, name: query, creator_id: user.id, namespace: user.namespace) } - let!(:pre) { create(:empty_project, name: "pre_#{query}", creator_id: user.id, namespace: user.namespace) } - let!(:post) { create(:empty_project, name: "#{query}_post", creator_id: user.id, namespace: user.namespace) } - let!(:pre_post) { create(:empty_project, name: "pre_#{query}_post", creator_id: user.id, namespace: user.namespace) } - let!(:unfound) { create(:empty_project, name: 'unfound', creator_id: user.id, namespace: user.namespace) } - let!(:internal) { create(:empty_project, :internal, name: "internal #{query}") } - let!(:unfound_internal) { create(:empty_project, :internal, name: 'unfound internal') } - let!(:public) { create(:empty_project, :public, name: "public #{query}") } - let!(:unfound_public) { create(:empty_project, :public, name: 'unfound public') } - let!(:one_dot_two) { create(:empty_project, :public, name: "one.dot.two") } - - shared_examples_for 'project search response' do |args = {}| - it 'returns project search responses' do - get api("/projects/search/#{args[:query]}", current_user) - - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.size).to eq(args[:results]) - json_response.each { |project| expect(project['name']).to match(args[:match_regex] || /.*#{args[:query]}.*/) } - end - end - - context 'when unauthenticated' do - it_behaves_like 'project search response', query: 'query', results: 1 do - let(:current_user) { nil } - end - end - - context 'when authenticated' do - it_behaves_like 'project search response', query: 'query', results: 6 do - let(:current_user) { user } - end - it_behaves_like 'project search response', query: 'one.dot.two', results: 1 do - let(:current_user) { user } - end - end - - context 'when authenticated as a different user' do - it_behaves_like 'project search response', query: 'query', results: 2, match_regex: /(internal|public) query/ do - let(:current_user) { user2 } - end - end - end - describe 'PUT /projects/:id' do before { project } before { user } diff --git a/spec/requests/api/v3/issues_spec.rb b/spec/requests/api/v3/issues_spec.rb new file mode 100644 index 00000000000..33a127de98a --- /dev/null +++ b/spec/requests/api/v3/issues_spec.rb @@ -0,0 +1,1259 @@ +require 'spec_helper' + +describe API::V3::Issues, api: true do + include ApiHelpers + include EmailHelpers + + let(:user) { create(:user) } + let(:user2) { create(:user) } + let(:non_member) { create(:user) } + let(:guest) { create(:user) } + let(:author) { create(:author) } + let(:assignee) { create(:assignee) } + let(:admin) { create(:user, :admin) } + let!(:project) { create(:empty_project, :public, creator_id: user.id, namespace: user.namespace ) } + let!(:closed_issue) do + create :closed_issue, + author: user, + assignee: user, + project: project, + state: :closed, + 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, + 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, + created_at: generate(:issue_created_at), + updated_at: 1.hour.ago + end + let!(:label) do + create(:label, title: 'label', color: '#FFAABB', project: project) + end + let!(:label_link) { create(:label_link, label: label, target: issue) } + let!(:milestone) { create(:milestone, title: '1.0.0', project: project) } + let!(:empty_milestone) do + create(:milestone, title: '2.0.0', project: project) + end + let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) } + + let(:no_milestone_title) { URI.escape(Milestone::None.title) } + + before do + project.team << [user, :reporter] + project.team << [guest, :guest] + end + + describe "GET /issues" do + context "when unauthenticated" do + it "returns authentication error" do + get v3_api("/issues") + + expect(response).to have_http_status(401) + end + end + + context "when authenticated" do + it "returns an array of issues" do + get v3_api("/issues", user) + + 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 'returns an array of closed issues' do + get v3_api('/issues?state=closed', user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(closed_issue.id) + end + + it 'returns an array of opened issues' do + get v3_api('/issues?state=opened', user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(issue.id) + end + + it 'returns an array of all issues' do + get v3_api('/issues?state=all', user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.first['id']).to eq(issue.id) + expect(json_response.second['id']).to eq(closed_issue.id) + end + + it 'returns an array of labeled issues' do + get v3_api("/issues?labels=#{label.title}", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['labels']).to eq([label.title]) + end + + it 'returns an array of labeled issues when at least one label matches' do + get v3_api("/issues?labels=#{label.title},foo,bar", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['labels']).to eq([label.title]) + end + + it 'returns an empty array if no issue matches labels' do + get v3_api('/issues?labels=foo,bar', user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + + it 'returns an array of labeled issues matching given state' do + get v3_api("/issues?labels=#{label.title}&state=opened", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['labels']).to eq([label.title]) + expect(json_response.first['state']).to eq('opened') + end + + it 'returns an empty array if no issue matches labels and state filters' do + get v3_api("/issues?labels=#{label.title}&state=closed", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + + it 'returns an empty array if no issue matches milestone' do + get v3_api("/issues?milestone=#{empty_milestone.title}", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + + it 'returns an empty array if milestone does not exist' do + get v3_api("/issues?milestone=foo", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + + it 'returns an array of issues in given milestone' do + get v3_api("/issues?milestone=#{milestone.title}", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.first['id']).to eq(issue.id) + expect(json_response.second['id']).to eq(closed_issue.id) + end + + it 'returns an array of issues matching state in milestone' do + get v3_api("/issues?milestone=#{milestone.title}", user), + '&state=closed' + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(closed_issue.id) + end + + it 'returns an array of issues with no milestone' do + get v3_api("/issues?milestone=#{no_milestone_title}", author) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(confidential_issue.id) + end + + it 'sorts by created_at descending by default' do + get v3_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 v3_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 v3_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 v3_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 + + describe "GET /groups/:id/issues" do + let!(:group) { create(:group) } + let!(:group_project) { create(:empty_project, :public, creator_id: user.id, namespace: group) } + let!(:group_closed_issue) do + create :closed_issue, + author: user, + assignee: user, + project: group_project, + state: :closed, + milestone: group_milestone, + updated_at: 3.hours.ago + end + let!(:group_confidential_issue) do + create :issue, + :confidential, + project: group_project, + author: author, + assignee: assignee, + updated_at: 2.hours.ago + end + let!(:group_issue) do + create :issue, + author: user, + assignee: user, + project: group_project, + milestone: group_milestone, + updated_at: 1.hour.ago + end + let!(:group_label) do + create(:label, title: 'group_lbl', color: '#FFAABB', project: group_project) + end + let!(:group_label_link) { create(:label_link, label: group_label, target: group_issue) } + let!(:group_milestone) { create(:milestone, title: '3.0.0', project: group_project) } + let!(:group_empty_milestone) do + create(:milestone, title: '4.0.0', project: group_project) + end + let!(:group_note) { create(:note_on_issue, author: user, project: group_project, noteable: group_issue) } + + before do + group_project.team << [user, :reporter] + end + let(:base_url) { "/groups/#{group.id}/issues" } + + it 'returns group issues without confidential issues for non project members' do + get v3_api(base_url, non_member) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['title']).to eq(group_issue.title) + end + + it 'returns group confidential issues for author' do + get v3_api(base_url, author) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + end + + it 'returns group confidential issues for assignee' do + get v3_api(base_url, assignee) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + end + + it 'returns group issues with confidential issues for project members' do + get v3_api(base_url, user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + end + + it 'returns group confidential issues for admin' do + get v3_api(base_url, admin) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + end + + it 'returns an array of labeled group issues' do + get v3_api("#{base_url}?labels=#{group_label.title}", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['labels']).to eq([group_label.title]) + end + + it 'returns an array of labeled group issues where all labels match' do + get v3_api("#{base_url}?labels=#{group_label.title},foo,bar", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + + it 'returns an empty array if no group issue matches labels' do + get v3_api("#{base_url}?labels=foo,bar", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + + it 'returns an empty array if no issue matches milestone' do + get v3_api("#{base_url}?milestone=#{group_empty_milestone.title}", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + + it 'returns an empty array if milestone does not exist' do + get v3_api("#{base_url}?milestone=foo", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + + it 'returns an array of issues in given milestone' do + get v3_api("#{base_url}?milestone=#{group_milestone.title}", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(group_issue.id) + end + + it 'returns an array of issues matching state in milestone' do + get v3_api("#{base_url}?milestone=#{group_milestone.title}", user), + '&state=closed' + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(group_closed_issue.id) + end + + it 'returns an array of issues with no milestone' do + get v3_api("#{base_url}?milestone=#{no_milestone_title}", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(group_confidential_issue.id) + end + + it 'sorts by created_at descending by default' do + get v3_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 v3_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 v3_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 v3_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 + let(:base_url) { "/projects/#{project.id}" } + + it "returns 404 on private projects for other users" do + private_project = create(:empty_project, :private) + create(:issue, project: private_project) + + get v3_api("/projects/#{private_project.id}/issues", non_member) + + expect(response).to have_http_status(404) + end + + it 'returns no issues when user has access to project but not issues' do + restricted_project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) + create(:issue, project: restricted_project) + + get v3_api("/projects/#{restricted_project.id}/issues", non_member) + + expect(json_response).to eq([]) + end + + it 'returns project issues without confidential issues for non project members' do + get v3_api("#{base_url}/issues", non_member) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.first['title']).to eq(issue.title) + end + + it 'returns project issues without confidential issues for project members with guest role' do + get v3_api("#{base_url}/issues", guest) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.first['title']).to eq(issue.title) + end + + it 'returns project confidential issues for author' do + get v3_api("#{base_url}/issues", author) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + expect(json_response.first['title']).to eq(issue.title) + end + + it 'returns project confidential issues for assignee' do + get v3_api("#{base_url}/issues", assignee) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + expect(json_response.first['title']).to eq(issue.title) + end + + it 'returns project issues with confidential issues for project members' do + get v3_api("#{base_url}/issues", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + expect(json_response.first['title']).to eq(issue.title) + end + + it 'returns project confidential issues for admin' do + get v3_api("#{base_url}/issues", admin) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + expect(json_response.first['title']).to eq(issue.title) + end + + it 'returns an array of labeled project issues' do + get v3_api("#{base_url}/issues?labels=#{label.title}", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['labels']).to eq([label.title]) + end + + it 'returns an array of labeled project issues where all labels match' do + get v3_api("#{base_url}/issues?labels=#{label.title},foo,bar", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['labels']).to eq([label.title]) + end + + it 'returns an empty array if no project issue matches labels' do + get v3_api("#{base_url}/issues?labels=foo,bar", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + + it 'returns an empty array if no issue matches milestone' do + get v3_api("#{base_url}/issues?milestone=#{empty_milestone.title}", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + + it 'returns an empty array if milestone does not exist' do + get v3_api("#{base_url}/issues?milestone=foo", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + + it 'returns an array of issues in given milestone' do + get v3_api("#{base_url}/issues?milestone=#{milestone.title}", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.first['id']).to eq(issue.id) + expect(json_response.second['id']).to eq(closed_issue.id) + end + + it 'returns an array of issues matching state in milestone' do + get v3_api("#{base_url}/issues?milestone=#{milestone.title}", user), + '&state=closed' + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(closed_issue.id) + end + + it 'returns an array of issues with no milestone' do + get v3_api("#{base_url}/issues?milestone=#{no_milestone_title}", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(confidential_issue.id) + end + + it 'sorts by created_at descending by default' do + get v3_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 v3_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 v3_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 v3_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 + it 'exposes known attributes' do + get v3_api("/projects/#{project.id}/issues/#{issue.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['id']).to eq(issue.id) + expect(json_response['iid']).to eq(issue.iid) + expect(json_response['project_id']).to eq(issue.project.id) + expect(json_response['title']).to eq(issue.title) + expect(json_response['description']).to eq(issue.description) + expect(json_response['state']).to eq(issue.state) + expect(json_response['created_at']).to be_present + expect(json_response['updated_at']).to be_present + expect(json_response['labels']).to eq(issue.label_names) + 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 + get v3_api("/projects/#{project.id}/issues/#{issue.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['title']).to eq(issue.title) + expect(json_response['iid']).to eq(issue.iid) + end + + it 'returns a project issue by iid' do + get v3_api("/projects/#{project.id}/issues?iid=#{issue.iid}", user) + + expect(response.status).to eq 200 + expect(json_response.length).to eq 1 + expect(json_response.first['title']).to eq issue.title + expect(json_response.first['id']).to eq issue.id + expect(json_response.first['iid']).to eq issue.iid + end + + it 'returns an empty array for an unknown project issue iid' do + get v3_api("/projects/#{project.id}/issues?iid=#{issue.iid + 10}", user) + + expect(response.status).to eq 200 + expect(json_response.length).to eq 0 + end + + it "returns 404 if issue id not found" do + get v3_api("/projects/#{project.id}/issues/54321", user) + + expect(response).to have_http_status(404) + end + + context 'confidential issues' do + it "returns 404 for non project members" do + get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member) + + expect(response).to have_http_status(404) + end + + it "returns 404 for project members with guest role" do + get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest) + + expect(response).to have_http_status(404) + end + + it "returns confidential issue for project members" do + get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['title']).to eq(confidential_issue.title) + expect(json_response['iid']).to eq(confidential_issue.iid) + end + + it "returns confidential issue for author" do + get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", author) + + expect(response).to have_http_status(200) + expect(json_response['title']).to eq(confidential_issue.title) + expect(json_response['iid']).to eq(confidential_issue.iid) + end + + it "returns confidential issue for assignee" do + get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", assignee) + + expect(response).to have_http_status(200) + expect(json_response['title']).to eq(confidential_issue.title) + expect(json_response['iid']).to eq(confidential_issue.iid) + end + + it "returns confidential issue for admin" do + get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin) + + expect(response).to have_http_status(200) + expect(json_response['title']).to eq(confidential_issue.title) + expect(json_response['iid']).to eq(confidential_issue.iid) + end + end + end + + describe "POST /projects/:id/issues" do + it 'creates a new project issue' do + post v3_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 v3_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 v3_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 v3_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 v3_api("/projects/#{project.id}/issues", user), + title: 'new issue', confidential: 'foo' + + expect(response).to have_http_status(400) + expect(json_response['error']).to eq('confidential is invalid') + end + + it "sends notifications for subscribers of newly added labels" do + label = project.labels.first + label.toggle_subscription(user2, project) + + perform_enqueued_jobs do + post v3_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 + post v3_api("/projects/#{project.id}/issues", user), labels: 'label, label2' + + expect(response).to have_http_status(400) + end + + it 'allows special label names' do + post v3_api("/projects/#{project.id}/issues", user), + title: 'new issue', + labels: 'label, label?, label&foo, ?, &' + + expect(response.status).to eq(201) + expect(json_response['labels']).to include 'label' + expect(json_response['labels']).to include 'label?' + expect(json_response['labels']).to include 'label&foo' + expect(json_response['labels']).to include '?' + expect(json_response['labels']).to include '&' + end + + it 'returns 400 if title is too long' do + post v3_api("/projects/#{project.id}/issues", user), + title: 'g' * 256 + + expect(response).to have_http_status(400) + expect(json_response['message']['title']).to eq([ + 'is too long (maximum is 255 characters)' + ]) + end + + context 'resolving issues in a merge request' do + let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first } + let(:merge_request) { discussion.noteable } + let(:project) { merge_request.source_project } + before do + project.team << [user, :master] + post v3_api("/projects/#{project.id}/issues", user), + title: 'New Issue', + merge_request_for_resolving_discussions: merge_request.iid + end + + it 'creates a new project issue' do + expect(response).to have_http_status(:created) + end + + it 'resolves the discussions in a merge request' do + discussion.first_note.reload + + expect(discussion.resolved?).to be(true) + end + + it 'assigns a description to the issue mentioning the merge request' do + expect(json_response['description']).to include(merge_request.to_reference) + end + end + + context 'with due date' do + it 'creates a new project issue' do + due_date = 2.weeks.from_now.strftime('%Y-%m-%d') + + post v3_api("/projects/#{project.id}/issues", user), + title: 'new issue', due_date: due_date + + 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['due_date']).to eq(due_date) + end + end + + context 'when an admin or owner makes the request' do + it 'accepts the creation date to be set' do + creation_time = 2.weeks.ago + post v3_api("/projects/#{project.id}/issues", user), + title: 'new issue', labels: 'label, label2', created_at: creation_time + + expect(response).to have_http_status(201) + expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time) + end + end + + context 'the user can only read the issue' do + it 'cannot create new labels' do + expect do + post v3_api("/projects/#{project.id}/issues", non_member), title: 'new issue', labels: 'label, label2' + end.not_to change { project.labels.count } + end + end + end + + describe 'POST /projects/:id/issues with spam filtering' do + before do + 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 + { + title: 'new issue', + description: 'content here', + labels: 'label, label2' + } + end + + it "does not create a new project issue" do + expect { post v3_api("/projects/#{project.id}/issues", user), params }.not_to change(Issue, :count) + + expect(response).to have_http_status(400) + expect(json_response['message']).to eq({ "error" => "Spam detected" }) + + spam_logs = SpamLog.all + + expect(spam_logs.count).to eq(1) + expect(spam_logs[0].title).to eq('new issue') + expect(spam_logs[0].description).to eq('content here') + expect(spam_logs[0].user).to eq(user) + expect(spam_logs[0].noteable_type).to eq('Issue') + end + end + + describe "PUT /projects/:id/issues/:issue_id to update only title" do + it "updates a project issue" do + put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), + title: 'updated title' + + expect(response).to have_http_status(200) + expect(json_response['title']).to eq('updated title') + end + + it "returns 404 error if issue id not found" do + put v3_api("/projects/#{project.id}/issues/44444", user), + title: 'updated title' + + expect(response).to have_http_status(404) + end + + it 'allows special label names' do + put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), + title: 'updated title', + labels: 'label, label?, label&foo, ?, &' + + expect(response.status).to eq(200) + expect(json_response['labels']).to include 'label' + expect(json_response['labels']).to include 'label?' + expect(json_response['labels']).to include 'label&foo' + expect(json_response['labels']).to include '?' + expect(json_response['labels']).to include '&' + end + + context 'confidential issues' do + it "returns 403 for non project members" do + put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member), + title: 'updated title' + + expect(response).to have_http_status(403) + end + + it "returns 403 for project members with guest role" do + put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest), + title: 'updated title' + + expect(response).to have_http_status(403) + end + + it "updates a confidential issue for project members" do + put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user), + title: 'updated title' + + expect(response).to have_http_status(200) + expect(json_response['title']).to eq('updated title') + end + + it "updates a confidential issue for author" do + put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", author), + title: 'updated title' + + expect(response).to have_http_status(200) + expect(json_response['title']).to eq('updated title') + end + + it "updates a confidential issue for admin" do + put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin), + title: 'updated title' + + expect(response).to have_http_status(200) + expect(json_response['title']).to eq('updated title') + end + + it 'sets an issue to confidential' do + put v3_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 v3_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 v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user), + confidential: 'foo' + + expect(response).to have_http_status(400) + expect(json_response['error']).to eq('confidential is invalid') + end + end + end + + describe 'PUT /projects/:id/issues/:issue_id to update labels' do + let!(:label) { create(:label, title: 'dummy', project: project) } + let!(:label_link) { create(:label_link, label: label, target: issue) } + + it 'does not update labels if not present' do + put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), + title: 'updated title' + + expect(response).to have_http_status(200) + 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, project) + + perform_enqueued_jobs do + put v3_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 v3_api("/projects/#{project.id}/issues/#{issue.id}", user), labels: '' + + expect(response).to have_http_status(200) + expect(json_response['labels']).to eq([]) + end + + it 'updates labels' do + put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), + labels: 'foo,bar' + + expect(response).to have_http_status(200) + expect(json_response['labels']).to include 'foo' + expect(json_response['labels']).to include 'bar' + end + + it 'allows special label names' do + put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), + labels: 'label:foo, label-bar,label_bar,label/bar,label?bar,label&bar,?,&' + + expect(response.status).to eq(200) + expect(json_response['labels']).to include 'label:foo' + expect(json_response['labels']).to include 'label-bar' + expect(json_response['labels']).to include 'label_bar' + expect(json_response['labels']).to include 'label/bar' + expect(json_response['labels']).to include 'label?bar' + expect(json_response['labels']).to include 'label&bar' + expect(json_response['labels']).to include '?' + expect(json_response['labels']).to include '&' + end + + it 'returns 400 if title is too long' do + put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), + title: 'g' * 256 + + expect(response).to have_http_status(400) + expect(json_response['message']['title']).to eq([ + 'is too long (maximum is 255 characters)' + ]) + end + end + + describe "PUT /projects/:id/issues/:issue_id to update state and label" do + it "updates a project issue" do + put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), + labels: 'label2', state_event: "close" + + expect(response).to have_http_status(200) + expect(json_response['labels']).to include 'label2' + expect(json_response['state']).to eq "closed" + end + + it 'reopens a project isssue' do + put v3_api("/projects/#{project.id}/issues/#{closed_issue.id}", user), state_event: 'reopen' + + expect(response).to have_http_status(200) + expect(json_response['state']).to eq 'reopened' + end + + context 'when an admin or owner makes the request' do + it 'accepts the update date to be set' do + update_time = 2.weeks.ago + put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), + labels: 'label3', state_event: 'close', updated_at: update_time + + expect(response).to have_http_status(200) + expect(json_response['labels']).to include 'label3' + expect(Time.parse(json_response['updated_at'])).to be_like_time(update_time) + end + end + end + + describe 'PUT /projects/:id/issues/:issue_id to update due date' do + it 'creates a new project issue' do + due_date = 2.weeks.from_now.strftime('%Y-%m-%d') + + put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), due_date: due_date + + expect(response).to have_http_status(200) + expect(json_response['due_date']).to eq(due_date) + end + end + + describe "DELETE /projects/:id/issues/:issue_id" do + it "rejects a non member from deleting an issue" do + delete v3_api("/projects/#{project.id}/issues/#{issue.id}", non_member) + + expect(response).to have_http_status(403) + end + + it "rejects a developer from deleting an issue" do + delete v3_api("/projects/#{project.id}/issues/#{issue.id}", author) + + expect(response).to have_http_status(403) + end + + context "when the user is project owner" do + let(:owner) { create(:user) } + let(:project) { create(:empty_project, namespace: owner.namespace) } + + it "deletes the issue if an admin requests it" do + delete v3_api("/projects/#{project.id}/issues/#{issue.id}", owner) + + expect(response).to have_http_status(200) + expect(json_response['state']).to eq 'opened' + end + end + + context 'when issue does not exist' do + it 'returns 404 when trying to move an issue' do + delete v3_api("/projects/#{project.id}/issues/123", user) + + expect(response).to have_http_status(404) + end + end + end + + describe '/projects/:id/issues/:issue_id/move' do + let!(:target_project) { create(:empty_project, path: 'project2', creator_id: user.id, namespace: user.namespace ) } + let!(:target_project2) { create(:empty_project, creator_id: non_member.id, namespace: non_member.namespace ) } + + it 'moves an issue' do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user), + to_project_id: target_project.id + + expect(response).to have_http_status(201) + expect(json_response['project_id']).to eq(target_project.id) + end + + context 'when source and target projects are the same' do + it 'returns 400 when trying to move an issue' do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user), + to_project_id: project.id + + expect(response).to have_http_status(400) + expect(json_response['message']).to eq('Cannot move issue to project it originates from!') + end + end + + context 'when the user does not have the permission to move issues' do + it 'returns 400 when trying to move an issue' do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user), + to_project_id: target_project2.id + + expect(response).to have_http_status(400) + expect(json_response['message']).to eq('Cannot move issue due to insufficient permissions!') + end + end + + it 'moves the issue to another namespace if I am admin' do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", admin), + to_project_id: target_project2.id + + expect(response).to have_http_status(201) + expect(json_response['project_id']).to eq(target_project2.id) + end + + context 'when issue does not exist' do + it 'returns 404 when trying to move an issue' do + post v3_api("/projects/#{project.id}/issues/123/move", user), + to_project_id: target_project.id + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Issue Not Found') + end + end + + context 'when source project does not exist' do + it 'returns 404 when trying to move an issue' do + post v3_api("/projects/123/issues/#{issue.id}/move", user), + to_project_id: target_project.id + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Project Not Found') + end + end + + context 'when target project does not exist' do + it 'returns 404 when trying to move an issue' do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user), + to_project_id: 123 + + expect(response).to have_http_status(404) + end + end + end + + describe 'POST :id/issues/:issue_id/subscription' do + it 'subscribes to an issue' do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2) + + expect(response).to have_http_status(201) + expect(json_response['subscribed']).to eq(true) + end + + it 'returns 304 if already subscribed' do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user) + + expect(response).to have_http_status(304) + end + + it 'returns 404 if the issue is not found' do + post v3_api("/projects/#{project.id}/issues/123/subscription", user) + + expect(response).to have_http_status(404) + end + + it 'returns 404 if the issue is confidential' do + post v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscription", non_member) + + expect(response).to have_http_status(404) + end + end + + describe 'DELETE :id/issues/:issue_id/subscription' do + it 'unsubscribes from an issue' do + delete v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user) + + expect(response).to have_http_status(200) + expect(json_response['subscribed']).to eq(false) + end + + it 'returns 304 if not subscribed' do + delete v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2) + + expect(response).to have_http_status(304) + end + + it 'returns 404 if the issue is not found' do + delete v3_api("/projects/#{project.id}/issues/123/subscription", user) + + expect(response).to have_http_status(404) + end + + it 'returns 404 if the issue is confidential' do + delete v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscription", non_member) + + expect(response).to have_http_status(404) + end + end + + describe 'time tracking endpoints' do + let(:issuable) { issue } + + include_examples 'time tracking endpoints', 'issue' + end +end diff --git a/spec/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb new file mode 100644 index 00000000000..b94e1ef4ced --- /dev/null +++ b/spec/requests/api/v3/merge_requests_spec.rb @@ -0,0 +1,726 @@ +require "spec_helper" + +describe API::MergeRequests, api: true do + include ApiHelpers + let(:base_time) { Time.now } + let(:user) { create(:user) } + let(:admin) { create(:user, :admin) } + let(:non_member) { create(:user) } + let!(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace) } + let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, title: "Test", created_at: base_time) } + let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_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, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') } + let(:milestone) { create(:milestone, title: '1.0.0', project: project) } + + before do + project.team << [user, :reporter] + end + + describe "GET /projects/:id/merge_requests" do + context "when unauthenticated" do + it "returns authentication error" do + get v3_api("/projects/#{project.id}/merge_requests") + expect(response).to have_http_status(401) + end + end + + context "when authenticated" do + it "returns an array of all merge_requests" do + get v3_api("/projects/#{project.id}/merge_requests", user) + expect(response).to have_http_status(200) + 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 + get v3_api("/projects/#{project.id}/merge_requests?state", user) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + expect(json_response.last['title']).to eq(merge_request.title) + end + + it "returns an array of open merge_requests" do + get v3_api("/projects/#{project.id}/merge_requests?state=opened", user) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.last['title']).to eq(merge_request.title) + end + + it "returns an array of closed merge_requests" do + get v3_api("/projects/#{project.id}/merge_requests?state=closed", user) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['title']).to eq(merge_request_closed.title) + end + + it "returns an array of merged merge_requests" do + get v3_api("/projects/#{project.id}/merge_requests?state=merged", user) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['title']).to eq(merge_request_merged.title) + end + + context "with ordering" do + before do + @mr_later = mr_with_later_created_and_updated_at_time + @mr_earlier = mr_with_earlier_created_and_updated_at_time + end + + it "returns an array of merge_requests in ascending order" do + get v3_api("/projects/#{project.id}/merge_requests?sort=asc", user) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + response_dates = json_response.map{ |merge_request| merge_request['created_at'] } + expect(response_dates).to eq(response_dates.sort) + end + + it "returns an array of merge_requests in descending order" do + get v3_api("/projects/#{project.id}/merge_requests?sort=desc", user) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + response_dates = json_response.map{ |merge_request| merge_request['created_at'] } + expect(response_dates).to eq(response_dates.sort.reverse) + end + + it "returns an array of merge_requests ordered by updated_at" do + get v3_api("/projects/#{project.id}/merge_requests?order_by=updated_at", user) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + response_dates = json_response.map{ |merge_request| merge_request['updated_at'] } + expect(response_dates).to eq(response_dates.sort.reverse) + end + + it "returns an array of merge_requests ordered by created_at" do + get v3_api("/projects/#{project.id}/merge_requests?order_by=created_at&sort=asc", user) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + response_dates = json_response.map{ |merge_request| merge_request['created_at'] } + expect(response_dates).to eq(response_dates.sort) + end + end + end + end + + describe "GET /projects/:id/merge_requests/:merge_request_id" do + it 'exposes known attributes' do + get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['id']).to eq(merge_request.id) + expect(json_response['iid']).to eq(merge_request.iid) + expect(json_response['project_id']).to eq(merge_request.project.id) + expect(json_response['title']).to eq(merge_request.title) + expect(json_response['description']).to eq(merge_request.description) + expect(json_response['state']).to eq(merge_request.state) + expect(json_response['created_at']).to be_present + expect(json_response['updated_at']).to be_present + expect(json_response['labels']).to eq(merge_request.label_names) + expect(json_response['milestone']).to be_nil + expect(json_response['assignee']).to be_a Hash + expect(json_response['author']).to be_a Hash + expect(json_response['target_branch']).to eq(merge_request.target_branch) + expect(json_response['source_branch']).to eq(merge_request.source_branch) + expect(json_response['upvotes']).to eq(0) + expect(json_response['downvotes']).to eq(0) + expect(json_response['source_project_id']).to eq(merge_request.source_project.id) + expect(json_response['target_project_id']).to eq(merge_request.target_project.id) + expect(json_response['work_in_progress']).to be_falsy + expect(json_response['merge_when_build_succeeds']).to be_falsy + expect(json_response['merge_status']).to eq('can_be_merged') + expect(json_response['should_close_merge_request']).to be_falsy + expect(json_response['force_close_merge_request']).to be_falsy + end + + it "returns merge_request" do + get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user) + expect(response).to have_http_status(200) + expect(json_response['title']).to eq(merge_request.title) + expect(json_response['iid']).to eq(merge_request.iid) + expect(json_response['work_in_progress']).to eq(false) + expect(json_response['merge_status']).to eq('can_be_merged') + expect(json_response['should_close_merge_request']).to be_falsy + expect(json_response['force_close_merge_request']).to be_falsy + end + + it 'returns merge_request by iid' do + url = "/projects/#{project.id}/merge_requests?iid=#{merge_request.iid}" + get v3_api(url, user) + expect(response.status).to eq 200 + expect(json_response.first['title']).to eq merge_request.title + expect(json_response.first['id']).to eq merge_request.id + end + + it 'returns merge_request by iid array' do + get v3_api("/projects/#{project.id}/merge_requests", user), iid: [merge_request.iid, merge_request_closed.iid] + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.first['title']).to eq merge_request_closed.title + expect(json_response.first['id']).to eq merge_request_closed.id + end + + it "returns a 404 error if merge_request_id not found" do + get v3_api("/projects/#{project.id}/merge_requests/999", user) + expect(response).to have_http_status(404) + end + + context 'Work in Progress' do + let!(:merge_request_wip) { create(:merge_request, author: user, assignee: user, source_project: project, target_project: project, title: "WIP: Test", created_at: base_time + 1.second) } + + it "returns merge_request" do + get v3_api("/projects/#{project.id}/merge_requests/#{merge_request_wip.id}", user) + expect(response).to have_http_status(200) + expect(json_response['work_in_progress']).to eq(true) + end + end + end + + describe 'GET /projects/:id/merge_requests/:merge_request_id/commits' do + it 'returns a 200 when merge request is valid' do + get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/commits", user) + commit = merge_request.commits.first + + expect(response.status).to eq 200 + expect(json_response.size).to eq(merge_request.commits.size) + expect(json_response.first['id']).to eq(commit.id) + expect(json_response.first['title']).to eq(commit.title) + end + + it 'returns a 404 when merge_request_id not found' do + get v3_api("/projects/#{project.id}/merge_requests/999/commits", user) + expect(response).to have_http_status(404) + end + end + + describe 'GET /projects/:id/merge_requests/:merge_request_id/changes' do + it 'returns the change information of the merge_request' do + get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/changes", user) + expect(response.status).to eq 200 + expect(json_response['changes'].size).to eq(merge_request.diffs.size) + end + + it 'returns a 404 when merge_request_id not found' do + get v3_api("/projects/#{project.id}/merge_requests/999/changes", user) + expect(response).to have_http_status(404) + end + end + + describe "POST /projects/:id/merge_requests" do + context 'between branches projects' do + it "returns merge_request" do + post v3_api("/projects/#{project.id}/merge_requests", user), + title: 'Test merge_request', + source_branch: 'feature_conflict', + target_branch: 'master', + author: user, + labels: 'label, label2', + milestone_id: milestone.id, + remove_source_branch: true + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq('Test merge_request') + expect(json_response['labels']).to eq(['label', 'label2']) + expect(json_response['milestone']['id']).to eq(milestone.id) + expect(json_response['force_remove_source_branch']).to be_truthy + end + + it "returns 422 when source_branch equals target_branch" do + post v3_api("/projects/#{project.id}/merge_requests", user), + title: "Test merge_request", source_branch: "master", target_branch: "master", author: user + expect(response).to have_http_status(422) + end + + it "returns 400 when source_branch is missing" do + post v3_api("/projects/#{project.id}/merge_requests", user), + title: "Test merge_request", target_branch: "master", author: user + expect(response).to have_http_status(400) + end + + it "returns 400 when target_branch is missing" do + post v3_api("/projects/#{project.id}/merge_requests", user), + title: "Test merge_request", source_branch: "markdown", author: user + expect(response).to have_http_status(400) + end + + it "returns 400 when title is missing" do + post v3_api("/projects/#{project.id}/merge_requests", user), + target_branch: 'master', source_branch: 'markdown' + expect(response).to have_http_status(400) + end + + it 'allows special label names' do + post v3_api("/projects/#{project.id}/merge_requests", user), + title: 'Test merge_request', + source_branch: 'markdown', + target_branch: 'master', + author: user, + labels: 'label, label?, label&foo, ?, &' + expect(response.status).to eq(201) + expect(json_response['labels']).to include 'label' + expect(json_response['labels']).to include 'label?' + expect(json_response['labels']).to include 'label&foo' + expect(json_response['labels']).to include '?' + expect(json_response['labels']).to include '&' + end + + context 'with existing MR' do + before do + post v3_api("/projects/#{project.id}/merge_requests", user), + title: 'Test merge_request', + source_branch: 'feature_conflict', + target_branch: 'master', + author: user + @mr = MergeRequest.all.last + end + + it 'returns 409 when MR already exists for source/target' do + expect do + post v3_api("/projects/#{project.id}/merge_requests", user), + title: 'New test merge_request', + source_branch: 'feature_conflict', + target_branch: 'master', + author: user + end.to change { MergeRequest.count }.by(0) + expect(response).to have_http_status(409) + end + end + end + + context 'forked projects' do + let!(:user2) { create(:user) } + let!(:fork_project) { create(:empty_project, forked_from_project: project, namespace: user2.namespace, creator_id: user2.id) } + let!(:unrelated_project) { create(:empty_project, namespace: create(:user).namespace, creator_id: user2.id) } + + before :each do |each| + fork_project.team << [user2, :reporter] + end + + it "returns merge_request" do + post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + title: 'Test merge_request', source_branch: "feature_conflict", target_branch: "master", + author: user2, target_project_id: project.id, description: 'Test description for Test merge_request' + expect(response).to have_http_status(201) + expect(json_response['title']).to eq('Test merge_request') + expect(json_response['description']).to eq('Test description for Test merge_request') + end + + it "does not return 422 when source_branch equals target_branch" do + expect(project.id).not_to eq(fork_project.id) + expect(fork_project.forked?).to be_truthy + expect(fork_project.forked_from_project).to eq(project) + post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + title: 'Test merge_request', source_branch: "master", target_branch: "master", author: user2, target_project_id: project.id + expect(response).to have_http_status(201) + expect(json_response['title']).to eq('Test merge_request') + end + + it "returns 400 when source_branch is missing" do + post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id + expect(response).to have_http_status(400) + end + + it "returns 400 when target_branch is missing" do + post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id + expect(response).to have_http_status(400) + end + + it "returns 400 when title is missing" do + post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: project.id + expect(response).to have_http_status(400) + end + + context 'when target_branch is specified' do + it 'returns 422 if not a forked project' do + post v3_api("/projects/#{project.id}/merge_requests", user), + title: 'Test merge_request', + target_branch: 'master', + source_branch: 'markdown', + author: user, + target_project_id: fork_project.id + expect(response).to have_http_status(422) + end + + it 'returns 422 if targeting a different fork' do + post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + title: 'Test merge_request', + target_branch: 'master', + source_branch: 'markdown', + author: user2, + target_project_id: unrelated_project.id + expect(response).to have_http_status(422) + end + end + + it "returns 201 when target_branch is specified and for the same project" do + post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: fork_project.id + expect(response).to have_http_status(201) + end + end + end + + describe "DELETE /projects/:id/merge_requests/:merge_request_id" do + context "when the user is developer" do + let(:developer) { create(:user) } + + before do + project.team << [developer, :developer] + end + + it "denies the deletion of the merge request" do + delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", developer) + expect(response).to have_http_status(403) + end + end + + context "when the user is project owner" do + it "destroys the merge request owners can destroy" do + delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user) + + expect(response).to have_http_status(200) + end + end + end + + describe "PUT /projects/:id/merge_requests/:merge_request_id/merge" do + let(:pipeline) { create(:ci_pipeline_without_jobs) } + + it "returns merge_request in case of success" do + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) + + expect(response).to have_http_status(200) + end + + it "returns 406 if branch can't be merged" do + allow_any_instance_of(MergeRequest). + to receive(:can_be_merged?).and_return(false) + + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) + + expect(response).to have_http_status(406) + expect(json_response['message']).to eq('Branch cannot be merged') + end + + it "returns 405 if merge_request is not open" do + merge_request.close + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) + expect(response).to have_http_status(405) + expect(json_response['message']).to eq('405 Method Not Allowed') + end + + it "returns 405 if merge_request is a work in progress" do + merge_request.update_attribute(:title, "WIP: #{merge_request.title}") + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) + expect(response).to have_http_status(405) + expect(json_response['message']).to eq('405 Method Not Allowed') + end + + it 'returns 405 if the build failed for a merge request that requires success' do + allow_any_instance_of(MergeRequest).to receive(:mergeable_ci_state?).and_return(false) + + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) + + expect(response).to have_http_status(405) + expect(json_response['message']).to eq('405 Method Not Allowed') + end + + it "returns 401 if user has no permissions to merge" do + user2 = create(:user) + project.team << [user2, :reporter] + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user2) + expect(response).to have_http_status(401) + expect(json_response['message']).to eq('401 Unauthorized') + end + + it "returns 409 if the SHA parameter doesn't match" do + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.diff_head_sha.reverse + + expect(response).to have_http_status(409) + expect(json_response['message']).to start_with('SHA does not match HEAD of source branch') + end + + it "succeeds if the SHA parameter matches" do + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.diff_head_sha + + expect(response).to have_http_status(200) + end + + it "enables merge when pipeline succeeds if the pipeline is active" do + allow_any_instance_of(MergeRequest).to receive(:head_pipeline).and_return(pipeline) + allow(pipeline).to receive(:active?).and_return(true) + + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), merge_when_build_succeeds: true + + expect(response).to have_http_status(200) + expect(json_response['title']).to eq('Test') + expect(json_response['merge_when_build_succeeds']).to eq(true) + end + end + + describe "PUT /projects/:id/merge_requests/:merge_request_id" do + context "to close a MR" do + it "returns merge_request" do + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: "close" + + expect(response).to have_http_status(200) + expect(json_response['state']).to eq('closed') + end + end + + it "updates title and returns merge_request" do + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), title: "New title" + expect(response).to have_http_status(200) + expect(json_response['title']).to eq('New title') + end + + it "updates description and returns merge_request" do + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), description: "New description" + expect(response).to have_http_status(200) + expect(json_response['description']).to eq('New description') + end + + it "updates milestone_id and returns merge_request" do + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), milestone_id: milestone.id + expect(response).to have_http_status(200) + expect(json_response['milestone']['id']).to eq(milestone.id) + end + + it "returns merge_request with renamed target_branch" do + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), target_branch: "wiki" + expect(response).to have_http_status(200) + expect(json_response['target_branch']).to eq('wiki') + end + + it "returns merge_request that removes the source branch" do + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), remove_source_branch: true + + expect(response).to have_http_status(200) + expect(json_response['force_remove_source_branch']).to be_truthy + end + + it 'allows special label names' do + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), + title: 'new issue', + labels: 'label, label?, label&foo, ?, &' + + expect(response.status).to eq(200) + expect(json_response['labels']).to include 'label' + expect(json_response['labels']).to include 'label?' + expect(json_response['labels']).to include 'label&foo' + expect(json_response['labels']).to include '?' + expect(json_response['labels']).to include '&' + end + + it 'does not update state when title is empty' do + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', title: nil + + merge_request.reload + expect(response).to have_http_status(400) + expect(merge_request.state).to eq('opened') + end + + it 'does not update state when target_branch is empty' do + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', target_branch: nil + + merge_request.reload + expect(response).to have_http_status(400) + expect(merge_request.state).to eq('opened') + end + end + + describe "POST /projects/:id/merge_requests/:merge_request_id/comments" do + it "returns comment" do + original_count = merge_request.notes.size + + post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user), note: "My comment" + + expect(response).to have_http_status(201) + expect(json_response['note']).to eq('My comment') + expect(json_response['author']['name']).to eq(user.name) + expect(json_response['author']['username']).to eq(user.username) + expect(merge_request.reload.notes.size).to eq(original_count + 1) + end + + it "returns 400 if note is missing" do + post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user) + expect(response).to have_http_status(400) + end + + it "returns 404 if note is attached to non existent merge request" do + post v3_api("/projects/#{project.id}/merge_requests/404/comments", user), + note: 'My comment' + expect(response).to have_http_status(404) + end + end + + describe "GET :id/merge_requests/:merge_request_id/comments" do + let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") } + let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") } + + it "returns merge_request comments ordered by created_at" do + get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.first['note']).to eq("a comment on a MR") + expect(json_response.first['author']['id']).to eq(user.id) + expect(json_response.last['note']).to eq("another comment on a MR") + end + + it "returns a 404 error if merge_request_id not found" do + get v3_api("/projects/#{project.id}/merge_requests/999/comments", user) + expect(response).to have_http_status(404) + end + end + + describe 'GET :id/merge_requests/:merge_request_id/closes_issues' do + it 'returns the issue that will be closed on merge' do + issue = create(:issue, project: project) + mr = merge_request.tap do |mr| + mr.update_attribute(:description, "Closes #{issue.to_reference(mr.project)}") + end + + get v3_api("/projects/#{project.id}/merge_requests/#{mr.id}/closes_issues", user) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(issue.id) + end + + it 'returns an empty array when there are no issues to be closed' do + get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", user) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + + it 'handles external issues' do + jira_project = create(:jira_project, :public, name: 'JIR_EXT1') + issue = ExternalIssue.new("#{jira_project.name}-123", jira_project) + merge_request = create(:merge_request, :simple, author: user, assignee: user, source_project: jira_project) + merge_request.update_attribute(:description, "Closes #{issue.to_reference(jira_project)}") + + get v3_api("/projects/#{jira_project.id}/merge_requests/#{merge_request.id}/closes_issues", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['title']).to eq(issue.title) + expect(json_response.first['id']).to eq(issue.id) + end + + it 'returns 403 if the user has no access to the merge request' do + project = create(:empty_project, :private) + merge_request = create(:merge_request, :simple, source_project: project) + guest = create(:user) + project.team << [guest, :guest] + + get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", guest) + + expect(response).to have_http_status(403) + end + end + + describe 'POST :id/merge_requests/:merge_request_id/subscription' do + it 'subscribes to a merge request' do + post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin) + + expect(response).to have_http_status(201) + expect(json_response['subscribed']).to eq(true) + end + + it 'returns 304 if already subscribed' do + post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user) + + expect(response).to have_http_status(304) + end + + it 'returns 404 if the merge request is not found' do + post v3_api("/projects/#{project.id}/merge_requests/123/subscription", user) + + expect(response).to have_http_status(404) + end + + it 'returns 403 if user has no access to read code' do + guest = create(:user) + project.team << [guest, :guest] + + post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest) + + expect(response).to have_http_status(403) + end + end + + describe 'DELETE :id/merge_requests/:merge_request_id/subscription' do + it 'unsubscribes from a merge request' do + delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user) + + expect(response).to have_http_status(200) + expect(json_response['subscribed']).to eq(false) + end + + it 'returns 304 if not subscribed' do + delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin) + + expect(response).to have_http_status(304) + end + + it 'returns 404 if the merge request is not found' do + post v3_api("/projects/#{project.id}/merge_requests/123/subscription", user) + + expect(response).to have_http_status(404) + end + + it 'returns 403 if user has no access to read code' do + guest = create(:user) + project.team << [guest, :guest] + + delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest) + + expect(response).to have_http_status(403) + end + end + + describe 'Time tracking' do + let(:issuable) { merge_request } + + include_examples 'time tracking endpoints', 'merge_request' + end + + def mr_with_later_created_and_updated_at_time + merge_request + merge_request.created_at += 1.hour + merge_request.updated_at += 30.minutes + merge_request.save + merge_request + end + + def mr_with_earlier_created_and_updated_at_time + merge_request_closed + merge_request_closed.created_at -= 1.hour + merge_request_closed.updated_at -= 30.minutes + merge_request_closed.save + merge_request_closed + end +end diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb new file mode 100644 index 00000000000..a495122bba7 --- /dev/null +++ b/spec/requests/api/v3/projects_spec.rb @@ -0,0 +1,1424 @@ +require 'spec_helper' + +describe API::V3::Projects, api: true do + include ApiHelpers + include Gitlab::CurrentSettings + + let(:user) { create(:user) } + let(:user2) { create(:user) } + let(:user3) { create(:user) } + let(:admin) { create(:admin) } + let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) } + let(:project2) { create(:empty_project, path: 'project2', creator_id: user.id, namespace: user.namespace) } + let(:snippet) { create(:project_snippet, :public, author: user, project: project, title: 'example') } + let(:project_member) { create(:project_member, :master, user: user, project: project) } + let(:project_member2) { create(:project_member, :developer, user: user3, project: project) } + let(:user4) { create(:user) } + let(:project3) do + create(:project, + :private, + :repository, + name: 'second_project', + path: 'second_project', + creator_id: user.id, + namespace: user.namespace, + merge_requests_enabled: false, + issues_enabled: false, wiki_enabled: false, + snippets_enabled: false) + end + let(:project_member3) do + create(:project_member, + user: user4, + project: project3, + access_level: ProjectMember::MASTER) + end + let(:project4) do + create(:empty_project, + name: 'third_project', + path: 'third_project', + creator_id: user4.id, + namespace: user4.namespace) + end + + describe 'GET /projects' do + before { project } + + context 'when unauthenticated' do + it 'returns authentication error' do + get v3_api('/projects') + expect(response).to have_http_status(401) + end + end + + context 'when authenticated as regular user' do + it 'returns an array of projects' do + get v3_api('/projects', user) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(project.name) + expect(json_response.first['owner']['username']).to eq(user.username) + end + + it 'includes the project labels as the tag_list' do + get v3_api('/projects', user) + expect(response.status).to eq 200 + expect(json_response).to be_an Array + expect(json_response.first.keys).to include('tag_list') + end + + it 'includes open_issues_count' do + get v3_api('/projects', user) + expect(response.status).to eq 200 + expect(json_response).to be_an Array + expect(json_response.first.keys).to include('open_issues_count') + end + + it 'does not include open_issues_count' do + project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) + + get v3_api('/projects', user) + expect(response.status).to eq 200 + expect(json_response).to be_an Array + expect(json_response.first.keys).not_to include('open_issues_count') + end + + context 'GET /projects?simple=true' do + it 'returns a simplified version of all the projects' do + expected_keys = ["id", "http_url_to_repo", "web_url", "name", "name_with_namespace", "path", "path_with_namespace"] + + get v3_api('/projects?simple=true', user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first.keys).to match_array expected_keys + end + end + + context 'and using search' do + it 'returns searched project' do + get v3_api('/projects', user), { search: project.name } + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + end + end + + context 'and using the visibility filter' do + it 'filters based on private visibility param' do + get v3_api('/projects', user), { visibility: 'private' } + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PRIVATE).count) + end + + it 'filters based on internal visibility param' do + get v3_api('/projects', user), { visibility: 'internal' } + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::INTERNAL).count) + end + + it 'filters based on public visibility param' do + get v3_api('/projects', user), { visibility: 'public' } + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PUBLIC).count) + end + end + + context 'and using sorting' do + before do + project2 + project3 + end + + it 'returns the correct order when sorted by id' do + get v3_api('/projects', user), { order_by: 'id', sort: 'desc' } + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['id']).to eq(project3.id) + end + end + end + end + + describe 'GET /projects/all' do + before { project } + + context 'when unauthenticated' do + it 'returns authentication error' do + get v3_api('/projects/all') + expect(response).to have_http_status(401) + end + end + + context 'when authenticated as regular user' do + it 'returns authentication error' do + get v3_api('/projects/all', user) + expect(response).to have_http_status(403) + end + end + + context 'when authenticated as admin' do + it 'returns an array of all projects' do + get v3_api('/projects/all', admin) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + + expect(json_response).to satisfy do |response| + response.one? do |entry| + entry.has_key?('permissions') && + entry['name'] == project.name && + entry['owner']['username'] == user.username + end + end + end + + it "does not include statistics by default" do + get v3_api('/projects/all', admin) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first).not_to include('statistics') + end + + it "includes statistics if requested" do + get v3_api('/projects/all', admin), statistics: true + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first).to include 'statistics' + end + end + end + + describe 'GET /projects/owned' do + before do + project3 + project4 + end + + context 'when unauthenticated' do + it 'returns authentication error' do + get v3_api('/projects/owned') + expect(response).to have_http_status(401) + end + end + + context 'when authenticated as project owner' do + it 'returns an array of projects the user owns' do + get v3_api('/projects/owned', user4) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(project4.name) + expect(json_response.first['owner']['username']).to eq(user4.username) + end + + it "does not include statistics by default" do + get v3_api('/projects/owned', user4) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first).not_to include('statistics') + end + + it "includes statistics if requested" do + attributes = { + commit_count: 23, + storage_size: 702, + repository_size: 123, + lfs_objects_size: 234, + build_artifacts_size: 345, + } + + project4.statistics.update!(attributes) + + get v3_api('/projects/owned', user4), statistics: true + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['statistics']).to eq attributes.stringify_keys + end + end + end + + describe 'GET /projects/visible' do + shared_examples_for 'visible projects response' do + it 'returns the visible projects' do + get v3_api('/projects/visible', current_user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.map { |p| p['id'] }).to contain_exactly(*projects.map(&:id)) + end + end + + let!(:public_project) { create(:empty_project, :public) } + before do + project + project2 + project3 + project4 + end + + context 'when unauthenticated' do + it_behaves_like 'visible projects response' do + let(:current_user) { nil } + let(:projects) { [public_project] } + end + end + + context 'when authenticated' do + it_behaves_like 'visible projects response' do + let(:current_user) { user } + let(:projects) { [public_project, project, project2, project3] } + end + end + + context 'when authenticated as a different user' do + it_behaves_like 'visible projects response' do + let(:current_user) { user2 } + let(:projects) { [public_project] } + end + end + end + + describe 'GET /projects/starred' do + let(:public_project) { create(:empty_project, :public) } + + before do + project_member2 + user3.update_attributes(starred_projects: [project, project2, project3, public_project]) + end + + it 'returns the starred projects viewable by the user' do + get v3_api('/projects/starred', user3) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.map { |project| project['id'] }).to contain_exactly(project.id, public_project.id) + end + end + + describe 'POST /projects' do + context 'maximum number of projects reached' do + it 'does not create new project and respond with 403' do + allow_any_instance_of(User).to receive(:projects_limit_left).and_return(0) + expect { post v3_api('/projects', user2), name: 'foo' }. + to change {Project.count}.by(0) + expect(response).to have_http_status(403) + end + end + + it 'creates new project without path and return 201' do + expect { post v3_api('/projects', user), name: 'foo' }. + to change { Project.count }.by(1) + expect(response).to have_http_status(201) + end + + it 'creates last project before reaching project limit' do + allow_any_instance_of(User).to receive(:projects_limit_left).and_return(1) + post v3_api('/projects', user2), name: 'foo' + expect(response).to have_http_status(201) + end + + it 'does not create new project without name and return 400' do + expect { post v3_api('/projects', user) }.not_to change { Project.count } + expect(response).to have_http_status(400) + end + + it "assigns attributes to project" do + project = attributes_for(:project, { + path: 'camelCasePath', + description: FFaker::Lorem.sentence, + issues_enabled: false, + merge_requests_enabled: false, + wiki_enabled: false, + only_allow_merge_if_build_succeeds: false, + request_access_enabled: true, + only_allow_merge_if_all_discussions_are_resolved: false + }) + + post v3_api('/projects', user), project + + project.each_pair do |k, v| + next if %i[has_external_issue_tracker 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 + project = attributes_for(:project, :public) + post v3_api('/projects', user), project + expect(json_response['public']).to be_truthy + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) + end + + it 'sets a project as public using :public' do + project = attributes_for(:project, { public: true }) + post v3_api('/projects', user), project + expect(json_response['public']).to be_truthy + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) + end + + it 'sets a project as internal' do + project = attributes_for(:project, :internal) + post v3_api('/projects', user), project + expect(json_response['public']).to be_falsey + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) + end + + it 'sets a project as internal overriding :public' do + project = attributes_for(:project, :internal, { public: true }) + post v3_api('/projects', user), project + expect(json_response['public']).to be_falsey + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) + end + + it 'sets a project as private' do + project = attributes_for(:project, :private) + post v3_api('/projects', user), project + expect(json_response['public']).to be_falsey + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) + end + + it 'sets a project as private using :public' do + project = attributes_for(:project, { public: false }) + post v3_api('/projects', user), project + 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 v3_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 v3_api('/projects', user), project + expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy + end + + it 'sets a project as allowing merge even if discussions are unresolved' do + project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: false }) + + post v3_api('/projects', user), project + + expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey + end + + it 'sets a project as allowing merge if only_allow_merge_if_all_discussions_are_resolved is nil' do + project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: nil) + + post v3_api('/projects', user), project + + expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey + end + + it 'sets a project as allowing merge only if all discussions are resolved' do + project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: true }) + + post v3_api('/projects', user), project + + expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy + end + + context 'when a visibility level is restricted' do + before do + @project = attributes_for(:project, { public: true }) + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) + end + + it 'does not allow a non-admin to use a restricted visibility level' do + post v3_api('/projects', user), @project + + expect(response).to have_http_status(400) + expect(json_response['message']['visibility_level'].first).to( + match('restricted by your GitLab administrator') + ) + end + + it 'allows an admin to override restricted visibility settings' do + post v3_api('/projects', admin), @project + expect(json_response['public']).to be_truthy + expect(json_response['visibility_level']).to( + eq(Gitlab::VisibilityLevel::PUBLIC) + ) + end + end + end + + describe 'POST /projects/user/:id' do + before { project } + before { admin } + + it 'should create new project without path and return 201' do + expect { post v3_api("/projects/user/#{user.id}", admin), name: 'foo' }.to change {Project.count}.by(1) + expect(response).to have_http_status(201) + end + + it 'responds with 400 on failure and not project' do + expect { post v3_api("/projects/user/#{user.id}", admin) }. + not_to change { Project.count } + + expect(response).to have_http_status(400) + expect(json_response['error']).to eq('name is missing') + end + + it 'assigns attributes to project' do + project = attributes_for(:project, { + description: FFaker::Lorem.sentence, + issues_enabled: false, + merge_requests_enabled: false, + wiki_enabled: false, + request_access_enabled: true + }) + + post v3_api("/projects/user/#{user.id}", admin), project + + expect(response).to have_http_status(201) + project.each_pair do |k, v| + next if %i[has_external_issue_tracker path].include?(k) + expect(json_response[k.to_s]).to eq(v) + end + end + + it 'sets a project as public' do + project = attributes_for(:project, :public) + post v3_api("/projects/user/#{user.id}", admin), project + + expect(response).to have_http_status(201) + expect(json_response['public']).to be_truthy + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) + end + + it 'sets a project as public using :public' do + project = attributes_for(:project, { public: true }) + post v3_api("/projects/user/#{user.id}", admin), project + + expect(response).to have_http_status(201) + expect(json_response['public']).to be_truthy + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) + end + + it 'sets a project as internal' do + project = attributes_for(:project, :internal) + post v3_api("/projects/user/#{user.id}", admin), project + + expect(response).to have_http_status(201) + expect(json_response['public']).to be_falsey + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) + end + + it 'sets a project as internal overriding :public' do + project = attributes_for(:project, :internal, { public: true }) + post v3_api("/projects/user/#{user.id}", admin), project + expect(response).to have_http_status(201) + expect(json_response['public']).to be_falsey + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) + end + + it 'sets a project as private' do + project = attributes_for(:project, :private) + post v3_api("/projects/user/#{user.id}", admin), project + expect(json_response['public']).to be_falsey + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) + end + + it 'sets a project as private using :public' do + project = attributes_for(:project, { public: false }) + post v3_api("/projects/user/#{user.id}", admin), project + 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 v3_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 v3_api("/projects/user/#{user.id}", admin), project + expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy + end + + it 'sets a project as allowing merge even if discussions are unresolved' do + project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: false }) + + post v3_api("/projects/user/#{user.id}", admin), project + + expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey + end + + it 'sets a project as allowing merge only if all discussions are resolved' do + project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: true }) + + post v3_api("/projects/user/#{user.id}", admin), project + + expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy + end + end + + describe "POST /projects/:id/uploads" do + before { project } + + it "uploads the file and returns its info" do + post v3_api("/projects/#{project.id}/uploads", user), file: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png") + + expect(response).to have_http_status(201) + expect(json_response['alt']).to eq("dk") + expect(json_response['url']).to start_with("/uploads/") + expect(json_response['url']).to end_with("/dk.png") + end + end + + describe 'GET /projects/:id' do + context 'when unauthenticated' do + it 'returns the public projects' do + public_project = create(:empty_project, :public) + + get v3_api("/projects/#{public_project.id}") + + expect(response).to have_http_status(200) + expect(json_response['id']).to eq(public_project.id) + expect(json_response['description']).to eq(public_project.description) + expect(json_response.keys).not_to include('permissions') + end + end + + context 'when authenticated' do + before do + project + project_member + end + + it 'returns a project by id' do + group = create(:group) + link = create(:project_group_link, project: project, group: group) + + get v3_api("/projects/#{project.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['id']).to eq(project.id) + expect(json_response['description']).to eq(project.description) + expect(json_response['default_branch']).to eq(project.default_branch) + expect(json_response['tag_list']).to be_an Array + expect(json_response['public']).to be_falsey + expect(json_response['archived']).to be_falsey + expect(json_response['visibility_level']).to be_present + expect(json_response['ssh_url_to_repo']).to be_present + expect(json_response['http_url_to_repo']).to be_present + expect(json_response['web_url']).to be_present + expect(json_response['owner']).to be_a Hash + expect(json_response['owner']).to be_a Hash + expect(json_response['name']).to eq(project.name) + expect(json_response['path']).to be_present + expect(json_response['issues_enabled']).to be_present + expect(json_response['merge_requests_enabled']).to be_present + expect(json_response['wiki_enabled']).to be_present + expect(json_response['builds_enabled']).to be_present + expect(json_response['snippets_enabled']).to be_present + expect(json_response['container_registry_enabled']).to be_present + expect(json_response['created_at']).to be_present + expect(json_response['last_activity_at']).to be_present + expect(json_response['shared_runners_enabled']).to be_present + expect(json_response['creator_id']).to be_present + expect(json_response['namespace']).to be_present + expect(json_response['avatar_url']).to be_nil + expect(json_response['star_count']).to be_present + expect(json_response['forks_count']).to be_present + expect(json_response['public_builds']).to be_present + expect(json_response['shared_with_groups']).to be_an Array + expect(json_response['shared_with_groups'].length).to eq(1) + expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id) + expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name) + expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access) + expect(json_response['only_allow_merge_if_build_succeeds']).to eq(project.only_allow_merge_if_build_succeeds) + expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved) + end + + it 'returns a project by path name' do + get v3_api("/projects/#{project.id}", user) + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(project.name) + end + + it 'returns a 404 error if not found' do + get v3_api('/projects/42', user) + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Project Not Found') + end + + it 'returns a 404 error if user is not a member' do + other_user = create(:user) + get v3_api("/projects/#{project.id}", other_user) + expect(response).to have_http_status(404) + end + + it 'handles users with dots' do + dot_user = create(:user, username: 'dot.user') + project = create(:empty_project, creator_id: dot_user.id, namespace: dot_user.namespace) + + get v3_api("/projects/#{dot_user.namespace.name}%2F#{project.path}", dot_user) + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(project.name) + end + + it 'exposes namespace fields' do + get v3_api("/projects/#{project.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['namespace']).to eq({ + 'id' => user.namespace.id, + 'name' => user.namespace.name, + 'path' => user.namespace.path, + 'kind' => user.namespace.kind, + }) + end + + describe 'permissions' do + context 'all projects' do + before { project.team << [user, :master] } + + it 'contains permission information' do + get v3_api("/projects", user) + + expect(response).to have_http_status(200) + expect(json_response.first['permissions']['project_access']['access_level']). + to eq(Gitlab::Access::MASTER) + expect(json_response.first['permissions']['group_access']).to be_nil + end + end + + context 'personal project' do + it 'sets project access and returns 200' do + project.team << [user, :master] + get v3_api("/projects/#{project.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['permissions']['project_access']['access_level']). + to eq(Gitlab::Access::MASTER) + expect(json_response['permissions']['group_access']).to be_nil + end + end + + context 'group project' do + let(:project2) { create(:empty_project, group: create(:group)) } + + before { project2.group.add_owner(user) } + + it 'sets the owner and return 200' do + get v3_api("/projects/#{project2.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['permissions']['project_access']).to be_nil + expect(json_response['permissions']['group_access']['access_level']). + to eq(Gitlab::Access::OWNER) + end + end + end + end + end + + describe 'GET /projects/:id/events' do + shared_examples_for 'project events response' do + it 'returns the project events' do + member = create(:user) + create(:project_member, :developer, user: member, project: project) + note = create(:note_on_issue, note: 'What an awesome day!', project: project) + EventCreateService.new.leave_note(note, note.author) + + get v3_api("/projects/#{project.id}/events", current_user) + + expect(response).to have_http_status(200) + + first_event = json_response.first + + expect(first_event['action_name']).to eq('commented on') + expect(first_event['note']['body']).to eq('What an awesome day!') + + last_event = json_response.last + + expect(last_event['action_name']).to eq('joined') + expect(last_event['project_id'].to_i).to eq(project.id) + expect(last_event['author_username']).to eq(member.username) + expect(last_event['author']['name']).to eq(member.name) + end + end + + context 'when unauthenticated' do + it_behaves_like 'project events response' do + let(:project) { create(:empty_project, :public) } + let(:current_user) { nil } + end + end + + context 'when authenticated' do + context 'valid request' do + it_behaves_like 'project events response' do + let(:current_user) { user } + end + end + + it 'returns a 404 error if not found' do + get v3_api('/projects/42/events', user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Project Not Found') + end + + it 'returns a 404 error if user is not a member' do + other_user = create(:user) + + get v3_api("/projects/#{project.id}/events", other_user) + + expect(response).to have_http_status(404) + end + end + end + + describe 'GET /projects/:id/users' do + shared_examples_for 'project users response' do + it 'returns the project users' do + member = create(:user) + create(:project_member, :developer, user: member, project: project) + + get v3_api("/projects/#{project.id}/users", current_user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + + first_user = json_response.first + + expect(first_user['username']).to eq(member.username) + expect(first_user['name']).to eq(member.name) + expect(first_user.keys).to contain_exactly(*%w[name username id state avatar_url web_url]) + end + end + + context 'when unauthenticated' do + it_behaves_like 'project users response' do + let(:project) { create(:empty_project, :public) } + let(:current_user) { nil } + end + end + + context 'when authenticated' do + context 'valid request' do + it_behaves_like 'project users response' do + let(:current_user) { user } + end + end + + it 'returns a 404 error if not found' do + get v3_api('/projects/42/users', user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Project Not Found') + end + + it 'returns a 404 error if user is not a member' do + other_user = create(:user) + + get v3_api("/projects/#{project.id}/users", other_user) + + expect(response).to have_http_status(404) + end + end + end + + describe 'GET /projects/:id/snippets' do + before { snippet } + + it 'returns an array of project snippets' do + get v3_api("/projects/#{project.id}/snippets", user) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['title']).to eq(snippet.title) + end + end + + describe 'GET /projects/:id/snippets/:snippet_id' do + it 'returns a project snippet' do + get v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user) + expect(response).to have_http_status(200) + expect(json_response['title']).to eq(snippet.title) + end + + it 'returns a 404 error if snippet id not found' do + get v3_api("/projects/#{project.id}/snippets/1234", user) + expect(response).to have_http_status(404) + end + end + + describe 'POST /projects/:id/snippets' do + it 'creates a new project snippet' do + post v3_api("/projects/#{project.id}/snippets", user), + title: 'v3_api test', file_name: 'sample.rb', code: 'test', + visibility_level: '0' + expect(response).to have_http_status(201) + expect(json_response['title']).to eq('v3_api test') + end + + it 'returns a 400 error if invalid snippet is given' do + post v3_api("/projects/#{project.id}/snippets", user) + expect(status).to eq(400) + end + end + + describe 'PUT /projects/:id/snippets/:snippet_id' do + it 'updates an existing project snippet' do + put v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user), + code: 'updated code' + expect(response).to have_http_status(200) + expect(json_response['title']).to eq('example') + expect(snippet.reload.content).to eq('updated code') + end + + it 'updates an existing project snippet with new title' do + put v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user), + title: 'other v3_api test' + expect(response).to have_http_status(200) + expect(json_response['title']).to eq('other v3_api test') + end + end + + describe 'DELETE /projects/:id/snippets/:snippet_id' do + before { snippet } + + it 'deletes existing project snippet' do + expect do + delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user) + end.to change { Snippet.count }.by(-1) + expect(response).to have_http_status(200) + end + + it 'returns 404 when deleting unknown snippet id' do + delete v3_api("/projects/#{project.id}/snippets/1234", user) + expect(response).to have_http_status(404) + end + end + + describe 'GET /projects/:id/snippets/:snippet_id/raw' do + it 'gets a raw project snippet' do + get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/raw", user) + expect(response).to have_http_status(200) + end + + it 'returns a 404 error if raw project snippet not found' do + get v3_api("/projects/#{project.id}/snippets/5555/raw", user) + expect(response).to have_http_status(404) + end + end + + describe :fork_admin do + let(:project_fork_target) { create(:empty_project) } + let(:project_fork_source) { create(:empty_project, :public) } + + describe 'POST /projects/:id/fork/:forked_from_id' do + let(:new_project_fork_source) { create(:empty_project, :public) } + + it "is not available for non admin users" do + post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", user) + expect(response).to have_http_status(403) + end + + it 'allows project to be forked from an existing project' do + expect(project_fork_target.forked?).not_to be_truthy + post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) + expect(response).to have_http_status(201) + project_fork_target.reload + expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id) + expect(project_fork_target.forked_project_link).not_to be_nil + expect(project_fork_target.forked?).to be_truthy + end + + it 'fails if forked_from project which does not exist' do + post v3_api("/projects/#{project_fork_target.id}/fork/9999", admin) + expect(response).to have_http_status(404) + end + + it 'fails with 409 if already forked' do + post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) + project_fork_target.reload + expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id) + post v3_api("/projects/#{project_fork_target.id}/fork/#{new_project_fork_source.id}", admin) + expect(response).to have_http_status(409) + project_fork_target.reload + expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id) + expect(project_fork_target.forked?).to be_truthy + end + end + + describe 'DELETE /projects/:id/fork' do + it "is not visible to users outside group" do + delete v3_api("/projects/#{project_fork_target.id}/fork", user) + expect(response).to have_http_status(404) + end + + context 'when users belong to project group' do + let(:project_fork_target) { create(:empty_project, group: create(:group)) } + + before do + project_fork_target.group.add_owner user + project_fork_target.group.add_developer user2 + end + + it 'is forbidden to non-owner users' do + delete v3_api("/projects/#{project_fork_target.id}/fork", user2) + expect(response).to have_http_status(403) + end + + it 'makes forked project unforked' do + post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) + project_fork_target.reload + expect(project_fork_target.forked_from_project).not_to be_nil + expect(project_fork_target.forked?).to be_truthy + delete v3_api("/projects/#{project_fork_target.id}/fork", admin) + expect(response).to have_http_status(200) + project_fork_target.reload + expect(project_fork_target.forked_from_project).to be_nil + expect(project_fork_target.forked?).not_to be_truthy + end + + it 'is idempotent if not forked' do + expect(project_fork_target.forked_from_project).to be_nil + delete v3_api("/projects/#{project_fork_target.id}/fork", admin) + expect(response).to have_http_status(304) + expect(project_fork_target.reload.forked_from_project).to be_nil + end + end + end + end + + describe "POST /projects/:id/share" do + let(:group) { create(:group) } + + it "shares project with group" do + expires_at = 10.days.from_now.to_date + + expect do + post v3_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).to have_http_status(201) + 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 + post v3_api("/projects/#{project.id}/share", user), group_access: Gitlab::Access::DEVELOPER + expect(response).to have_http_status(400) + end + + it "returns a 400 error when access level is not given" do + post v3_api("/projects/#{project.id}/share", user), group_id: group.id + expect(response).to have_http_status(400) + end + + it "returns a 400 error when sharing is disabled" do + project.namespace.update(share_with_group_lock: true) + post v3_api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER + expect(response).to have_http_status(400) + end + + it 'returns a 404 error when user cannot read group' do + private_group = create(:group, :private) + + post v3_api("/projects/#{project.id}/share", user), group_id: private_group.id, group_access: Gitlab::Access::DEVELOPER + + expect(response).to have_http_status(404) + end + + it 'returns a 404 error when group does not exist' do + post v3_api("/projects/#{project.id}/share", user), group_id: 1234, group_access: Gitlab::Access::DEVELOPER + + expect(response).to have_http_status(404) + end + + it "returns a 400 error when wrong params passed" do + post v3_api("/projects/#{project.id}/share", user), group_id: group.id, group_access: 1234 + + expect(response).to have_http_status(400) + expect(json_response['error']).to eq 'group_access does not have a valid value' + end + end + + describe 'DELETE /projects/:id/share/:group_id' do + it 'returns 204 when deleting a group share' do + group = create(:group, :public) + create(:project_group_link, group: group, project: project) + + delete v3_api("/projects/#{project.id}/share/#{group.id}", user) + + expect(response).to have_http_status(204) + expect(project.project_group_links).to be_empty + end + + it 'returns a 400 when group id is not an integer' do + delete v3_api("/projects/#{project.id}/share/foo", user) + + expect(response).to have_http_status(400) + end + + it 'returns a 404 error when group link does not exist' do + delete v3_api("/projects/#{project.id}/share/1234", user) + + expect(response).to have_http_status(404) + end + + it 'returns a 404 error when project does not exist' do + delete v3_api("/projects/123/share/1234", user) + + expect(response).to have_http_status(404) + end + end + + describe 'GET /projects/search/:query' do + let!(:query) { 'query'} + let!(:search) { create(:empty_project, name: query, creator_id: user.id, namespace: user.namespace) } + let!(:pre) { create(:empty_project, name: "pre_#{query}", creator_id: user.id, namespace: user.namespace) } + let!(:post) { create(:empty_project, name: "#{query}_post", creator_id: user.id, namespace: user.namespace) } + let!(:pre_post) { create(:empty_project, name: "pre_#{query}_post", creator_id: user.id, namespace: user.namespace) } + let!(:unfound) { create(:empty_project, name: 'unfound', creator_id: user.id, namespace: user.namespace) } + let!(:internal) { create(:empty_project, :internal, name: "internal #{query}") } + let!(:unfound_internal) { create(:empty_project, :internal, name: 'unfound internal') } + let!(:public) { create(:empty_project, :public, name: "public #{query}") } + let!(:unfound_public) { create(:empty_project, :public, name: 'unfound public') } + let!(:one_dot_two) { create(:empty_project, :public, name: "one.dot.two") } + + shared_examples_for 'project search response' do |args = {}| + it 'returns project search responses' do + get v3_api("/projects/search/#{args[:query]}", current_user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(args[:results]) + json_response.each { |project| expect(project['name']).to match(args[:match_regex] || /.*#{args[:query]}.*/) } + end + end + + context 'when unauthenticated' do + it_behaves_like 'project search response', query: 'query', results: 1 do + let(:current_user) { nil } + end + end + + context 'when authenticated' do + it_behaves_like 'project search response', query: 'query', results: 6 do + let(:current_user) { user } + end + it_behaves_like 'project search response', query: 'one.dot.two', results: 1 do + let(:current_user) { user } + end + end + + context 'when authenticated as a different user' do + it_behaves_like 'project search response', query: 'query', results: 2, match_regex: /(internal|public) query/ do + let(:current_user) { user2 } + end + end + end + + describe 'PUT /projects/:id' do + before { project } + before { user } + before { user3 } + before { user4 } + before { project3 } + before { project4 } + before { project_member3 } + before { project_member2 } + + context 'when unauthenticated' do + it 'returns authentication error' do + project_param = { name: 'bar' } + put v3_api("/projects/#{project.id}"), project_param + expect(response).to have_http_status(401) + end + end + + context 'when authenticated as project owner' do + it 'updates name' do + project_param = { name: 'bar' } + put v3_api("/projects/#{project.id}", user), project_param + expect(response).to have_http_status(200) + project_param.each_pair do |k, v| + expect(json_response[k.to_s]).to eq(v) + end + end + + it 'updates visibility_level' do + project_param = { visibility_level: 20 } + put v3_api("/projects/#{project3.id}", user), project_param + expect(response).to have_http_status(200) + project_param.each_pair do |k, v| + expect(json_response[k.to_s]).to eq(v) + end + end + + it 'updates visibility_level from public to private' do + project3.update_attributes({ visibility_level: Gitlab::VisibilityLevel::PUBLIC }) + project_param = { public: false } + put v3_api("/projects/#{project3.id}", user), project_param + expect(response).to have_http_status(200) + project_param.each_pair do |k, v| + expect(json_response[k.to_s]).to eq(v) + end + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) + end + + it 'does not update name to existing name' do + project_param = { name: project3.name } + put v3_api("/projects/#{project.id}", user), project_param + expect(response).to have_http_status(400) + 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 v3_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 v3_api("/projects/#{project3.id}", user), project_param + expect(response).to have_http_status(200) + project_param.each_pair do |k, v| + expect(json_response[k.to_s]).to eq(v) + end + end + end + + context 'when authenticated as project master' do + it 'updates path' do + project_param = { path: 'bar' } + put v3_api("/projects/#{project3.id}", user4), project_param + expect(response).to have_http_status(200) + project_param.each_pair do |k, v| + expect(json_response[k.to_s]).to eq(v) + end + end + + it 'updates other attributes' do + project_param = { issues_enabled: true, + wiki_enabled: true, + snippets_enabled: true, + merge_requests_enabled: true, + description: 'new description' } + + put v3_api("/projects/#{project3.id}", user4), project_param + expect(response).to have_http_status(200) + project_param.each_pair do |k, v| + expect(json_response[k.to_s]).to eq(v) + end + end + + it 'does not update path to existing path' do + project_param = { path: project.path } + put v3_api("/projects/#{project3.id}", user4), project_param + expect(response).to have_http_status(400) + expect(json_response['message']['path']).to eq(['has already been taken']) + end + + it 'does not update name' do + project_param = { name: 'bar' } + put v3_api("/projects/#{project3.id}", user4), project_param + expect(response).to have_http_status(403) + end + + it 'does not update visibility_level' do + project_param = { visibility_level: 20 } + put v3_api("/projects/#{project3.id}", user4), project_param + expect(response).to have_http_status(403) + end + end + + context 'when authenticated as project developer' do + it 'does not update other attributes' do + project_param = { path: 'bar', + issues_enabled: true, + wiki_enabled: true, + snippets_enabled: true, + merge_requests_enabled: true, + description: 'new description', + request_access_enabled: true } + put v3_api("/projects/#{project.id}", user3), project_param + expect(response).to have_http_status(403) + end + end + end + + describe 'POST /projects/:id/archive' do + context 'on an unarchived project' do + it 'archives the project' do + post v3_api("/projects/#{project.id}/archive", user) + + expect(response).to have_http_status(201) + expect(json_response['archived']).to be_truthy + end + end + + context 'on an archived project' do + before do + project.archive! + end + + it 'remains archived' do + post v3_api("/projects/#{project.id}/archive", user) + + expect(response).to have_http_status(201) + expect(json_response['archived']).to be_truthy + end + end + + context 'user without archiving rights to the project' do + before do + project.team << [user3, :developer] + end + + it 'rejects the action' do + post v3_api("/projects/#{project.id}/archive", user3) + + expect(response).to have_http_status(403) + end + end + end + + describe 'POST /projects/:id/unarchive' do + context 'on an unarchived project' do + it 'remains unarchived' do + post v3_api("/projects/#{project.id}/unarchive", user) + + expect(response).to have_http_status(201) + expect(json_response['archived']).to be_falsey + end + end + + context 'on an archived project' do + before do + project.archive! + end + + it 'unarchives the project' do + post v3_api("/projects/#{project.id}/unarchive", user) + + expect(response).to have_http_status(201) + expect(json_response['archived']).to be_falsey + end + end + + context 'user without archiving rights to the project' do + before do + project.team << [user3, :developer] + end + + it 'rejects the action' do + post v3_api("/projects/#{project.id}/unarchive", user3) + + expect(response).to have_http_status(403) + end + end + end + + describe 'POST /projects/:id/star' do + context 'on an unstarred project' do + it 'stars the project' do + expect { post v3_api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(1) + + expect(response).to have_http_status(201) + expect(json_response['star_count']).to eq(1) + end + end + + context 'on a starred project' do + before do + user.toggle_star(project) + project.reload + end + + it 'does not modify the star count' do + expect { post v3_api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count } + + expect(response).to have_http_status(304) + end + end + end + + describe 'DELETE /projects/:id/star' do + context 'on a starred project' do + before do + user.toggle_star(project) + project.reload + end + + it 'unstars the project' do + expect { delete v3_api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(-1) + + expect(response).to have_http_status(200) + expect(json_response['star_count']).to eq(0) + end + end + + context 'on an unstarred project' do + it 'does not modify the star count' do + expect { delete v3_api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count } + + expect(response).to have_http_status(304) + end + end + end + + describe 'DELETE /projects/:id' do + context 'when authenticated as user' do + it 'removes project' do + delete v3_api("/projects/#{project.id}", user) + expect(response).to have_http_status(200) + end + + it 'does not remove a project if not an owner' do + user3 = create(:user) + project.team << [user3, :developer] + delete v3_api("/projects/#{project.id}", user3) + expect(response).to have_http_status(403) + end + + it 'does not remove a non existing project' do + delete v3_api('/projects/1328', user) + expect(response).to have_http_status(404) + end + + it 'does not remove a project not attached to user' do + delete v3_api("/projects/#{project.id}", user2) + expect(response).to have_http_status(404) + end + end + + context 'when authenticated as admin' do + it 'removes any existing project' do + delete v3_api("/projects/#{project.id}", admin) + expect(response).to have_http_status(200) + end + + it 'does not remove a non existing project' do + delete v3_api('/projects/1328', admin) + expect(response).to have_http_status(404) + end + end + end +end |