diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-01-21 03:08:37 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-01-21 03:08:37 +0000 |
commit | 2399724614f3c4dcf3059038d997193830de93ee (patch) | |
tree | 3315c4453ef3efb5c1162911753436cad4f3e57d /spec/support/shared_examples/requests/api | |
parent | 6755df108b123ecc8ae330d7c7bf2f04fbf36a81 (diff) | |
download | gitlab-ce-2399724614f3c4dcf3059038d997193830de93ee.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/support/shared_examples/requests/api')
21 files changed, 1133 insertions, 14 deletions
diff --git a/spec/support/shared_examples/requests/api/award_emoji_todo_shared_examples.rb b/spec/support/shared_examples/requests/api/award_emoji_todo_shared_examples.rb new file mode 100644 index 00000000000..88ad37d232f --- /dev/null +++ b/spec/support/shared_examples/requests/api/award_emoji_todo_shared_examples.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +# Shared examples to that test code that creates AwardEmoji also mark Todos +# as done. +# +# The examples expect these to be defined in the calling spec: +# - `subject` the callable code that executes the creation of an AwardEmoji +# - `user` +# - `project` +RSpec.shared_examples 'creating award emojis marks Todos as done' do + using RSpec::Parameterized::TableSyntax + + before do + project.add_developer(user) + end + + where(:type, :expectation) do + :issue | true + :merge_request | true + :project_snippet | false + end + + with_them do + let(:project) { awardable.project } + let(:awardable) { create(type) } + let!(:todo) { create(:todo, target: awardable, project: project, user: user) } + + it do + subject + + expect(todo.reload.done?).to eq(expectation) + end + end + + # Notes have more complicated rules than other Todoables + describe 'for notes' do + let!(:todo) { create(:todo, target: awardable.noteable, project: project, user: user) } + + context 'regular Notes' do + let(:awardable) { create(:note, project: project) } + + it 'marks the Todo as done' do + subject + + expect(todo.reload.done?).to eq(true) + end + end + + context 'PersonalSnippet Notes' do + let(:awardable) { create(:note, noteable: create(:personal_snippet, author: user)) } + + it 'does not mark the Todo as done' do + subject + + expect(todo.reload.done?).to eq(false) + end + end + end +end diff --git a/spec/support/shared_examples/requests/api/boards_shared_examples.rb b/spec/support/shared_examples/requests/api/boards_shared_examples.rb new file mode 100644 index 00000000000..2bc79a2ef4d --- /dev/null +++ b/spec/support/shared_examples/requests/api/boards_shared_examples.rb @@ -0,0 +1,189 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'group and project boards' do |route_definition, ee = false| + let(:root_url) { route_definition.gsub(":id", board_parent.id.to_s) } + + before do + board_parent.add_reporter(user) + board_parent.add_guest(guest) + end + + def expect_schema_match_for(response, schema_file, ee) + if ee + expect(response).to match_response_schema(schema_file, dir: "ee") + else + expect(response).to match_response_schema(schema_file) + end + end + + it 'avoids N+1 queries' do + pat = create(:personal_access_token, user: user) + control = ActiveRecord::QueryRecorder.new { get api(root_url, personal_access_token: pat) } + + create(:milestone, "#{board_parent.class.name.underscore}": board_parent) + create(:board, "#{board_parent.class.name.underscore}": board_parent) + + expect { get api(root_url, personal_access_token: pat) }.not_to exceed_query_limit(control) + end + + describe "GET #{route_definition}" do + context "when unauthenticated" do + it "returns authentication error" do + get api(root_url) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context "when authenticated" do + it "returns the issue boards" do + get api(root_url, user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + + expect_schema_match_for(response, 'public_api/v4/boards', ee) + end + + describe "GET #{route_definition}/:board_id" do + let(:url) { "#{root_url}/#{board.id}" } + + it 'get a single board by id' do + get api(url, user) + + expect_schema_match_for(response, 'public_api/v4/board', ee) + end + end + end + end + + describe "GET #{route_definition}/:board_id/lists" do + let(:url) { "#{root_url}/#{board.id}/lists" } + + it 'returns issue board lists' do + get api(url, user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.first['label']['name']).to eq(dev_label.title) + end + + it 'returns 404 if board not found' do + get api("#{root_url}/22343/lists", user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + describe "GET #{route_definition}/:board_id/lists/:list_id" do + let(:url) { "#{root_url}/#{board.id}/lists" } + + it 'returns a list' do + get api("#{url}/#{dev_list.id}", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['id']).to eq(dev_list.id) + expect(json_response['label']['name']).to eq(dev_label.title) + expect(json_response['position']).to eq(1) + end + + it 'returns 404 if list not found' do + get api("#{url}/5324", user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + describe "POST #{route_definition}/lists" do + let(:url) { "#{root_url}/#{board.id}/lists" } + + it 'creates a new issue board list for labels' do + post api(url, user), params: { label_id: ux_label.id } + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['label']['name']).to eq(ux_label.title) + expect(json_response['position']).to eq(3) + end + + it 'returns 400 when creating a new list if label_id is invalid' do + post api(url, user), params: { label_id: 23423 } + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'returns 403 for members with guest role' do + put api("#{url}/#{test_list.id}", guest), params: { position: 1 } + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + describe "PUT #{route_definition}/:board_id/lists/:list_id to update only position" do + let(:url) { "#{root_url}/#{board.id}/lists" } + + it "updates a list" do + put api("#{url}/#{test_list.id}", user), params: { position: 1 } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['position']).to eq(1) + end + + it "returns 404 error if list id not found" do + put api("#{url}/44444", user), params: { position: 1 } + + expect(response).to have_gitlab_http_status(:not_found) + end + + it "returns 403 for members with guest role" do + put api("#{url}/#{test_list.id}", guest), params: { position: 1 } + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + describe "DELETE #{route_definition}/lists/:list_id" do + let(:url) { "#{root_url}/#{board.id}/lists" } + + it "rejects a non member from deleting a list" do + delete api("#{url}/#{dev_list.id}", non_member) + + expect(response).to have_gitlab_http_status(:forbidden) + end + + it "rejects a user with guest role from deleting a list" do + delete api("#{url}/#{dev_list.id}", guest) + + expect(response).to have_gitlab_http_status(:forbidden) + end + + it "returns 404 error if list id not found" do + delete api("#{url}/44444", user) + + expect(response).to have_gitlab_http_status(:not_found) + end + + context "when the user is parent owner" do + set(:owner) { create(:user) } + + before do + if board_parent.try(:namespace) + board_parent.update(namespace: owner.namespace) + else + board.resource_parent.add_owner(owner) + end + end + + it "deletes the list if an admin requests it" do + delete api("#{url}/#{dev_list.id}", owner) + + expect(response).to have_gitlab_http_status(:no_content) + end + + it_behaves_like '412 response' do + let(:request) { api("#{url}/#{dev_list.id}", owner) } + end + end + end +end diff --git a/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb b/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb new file mode 100644 index 00000000000..0f277c11913 --- /dev/null +++ b/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'rejected container repository access' do |user_type, status| + context "for #{user_type}" do + let(:api_user) { users[user_type] } + + it "returns #{status}" do + subject + + expect(response).to have_gitlab_http_status(status) + end + end +end + +RSpec.shared_examples 'returns repositories for allowed users' do |user_type, scope| + context "for #{user_type}" do + it 'returns a list of repositories' do + subject + + expect(json_response.length).to eq(2) + expect(json_response.map { |repository| repository['id'] }).to contain_exactly( + root_repository.id, test_repository.id) + expect(response.body).not_to include('tags') + end + + it 'returns a matching schema' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('registry/repositories') + end + + context 'with tags param' do + let(:url) { "/#{scope}s/#{object.id}/registry/repositories?tags=true" } + + before do + stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA latest), with_manifest: true) + stub_container_registry_tags(repository: test_repository.path, tags: %w(rootA latest), with_manifest: true) + end + + it 'returns a list of repositories and their tags' do + subject + + expect(json_response.length).to eq(2) + expect(json_response.map { |repository| repository['id'] }).to contain_exactly( + root_repository.id, test_repository.id) + expect(response.body).to include('tags') + end + + it 'returns a matching schema' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('registry/repositories') + end + end + end +end + +RSpec.shared_examples 'a gitlab tracking event' do |category, action| + it "creates a gitlab tracking event #{action}" do + expect(Gitlab::Tracking).to receive(:event).with(category, action, {}) + + subject + end +end diff --git a/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb b/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb index 22bd6e9cdf7..8cbf11b6de1 100644 --- a/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_examples 'custom attributes endpoints' do |attributable_name| +RSpec.shared_examples 'custom attributes endpoints' do |attributable_name| let!(:custom_attribute1) { attributable.custom_attributes.create key: 'foo', value: 'foo' } let!(:custom_attribute2) { attributable.custom_attributes.create key: 'bar', value: 'bar' } diff --git a/spec/support/shared_examples/requests/api/diff_discussions.rb b/spec/support/shared_examples/requests/api/diff_discussions_shared_examples.rb index 8ef3ed3f057..583475678f1 100644 --- a/spec/support/shared_examples/requests/api/diff_discussions.rb +++ b/spec/support/shared_examples/requests/api/diff_discussions_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_examples 'diff discussions API' do |parent_type, noteable_type, id_name| +RSpec.shared_examples 'diff discussions API' do |parent_type, noteable_type, id_name| describe "GET /#{parent_type}/:id/#{noteable_type}/:noteable_id/discussions" do it "includes diff discussions" do get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions", user) diff --git a/spec/support/shared_examples/requests/api/discussions.rb b/spec/support/shared_examples/requests/api/discussions_shared_examples.rb index 232c8d20025..939ea405724 100644 --- a/spec/support/shared_examples/requests/api/discussions.rb +++ b/spec/support/shared_examples/requests/api/discussions_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_examples 'with cross-reference system notes' do +RSpec.shared_examples 'with cross-reference system notes' do let(:merge_request) { create(:merge_request) } let(:project) { merge_request.project } let(:new_merge_request) { create(:merge_request) } @@ -54,7 +54,7 @@ shared_examples 'with cross-reference system notes' do end end -shared_examples 'discussions API' do |parent_type, noteable_type, id_name, can_reply_to_individual_notes: false| +RSpec.shared_examples 'discussions API' do |parent_type, noteable_type, id_name, can_reply_to_individual_notes: false| describe "GET /#{parent_type}/:id/#{noteable_type}/:noteable_id/discussions" do it "returns an array of discussions" do get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions", user) diff --git a/spec/support/shared_examples/requests/api/issuable_participants_examples.rb b/spec/support/shared_examples/requests/api/issuable_participants_examples.rb index 013b135235c..e442b988349 100644 --- a/spec/support/shared_examples/requests/api/issuable_participants_examples.rb +++ b/spec/support/shared_examples/requests/api/issuable_participants_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_examples 'issuable participants endpoint' do +RSpec.shared_examples 'issuable participants endpoint' do let(:area) { entity.class.name.underscore.pluralize } it 'returns participants' do diff --git a/spec/support/shared_examples/requests/api/issues/merge_requests_count_shared_examples.rb b/spec/support/shared_examples/requests/api/issues/merge_requests_count_shared_examples.rb index 90c1ed8d09b..971b21b5b32 100644 --- a/spec/support/shared_examples/requests/api/issues/merge_requests_count_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/issues/merge_requests_count_shared_examples.rb @@ -4,7 +4,7 @@ def get_issue json_response.is_a?(Array) ? json_response.detect {|issue| issue['id'] == target_issue.id} : json_response end -shared_examples 'accessible merge requests count' do +RSpec.shared_examples 'accessible merge requests count' do it 'returns anonymous accessible merge requests count' do get api(api_url), params: { scope: 'all' } diff --git a/spec/support/shared_examples/requests/api/issues_resolving_discussions_shared_examples.rb b/spec/support/shared_examples/requests/api/issues_resolving_discussions_shared_examples.rb new file mode 100644 index 00000000000..b748d5f5eea --- /dev/null +++ b/spec/support/shared_examples/requests/api/issues_resolving_discussions_shared_examples.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'creating an issue resolving discussions through the API' do + it 'creates a new project issue' do + expect(response).to have_gitlab_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 diff --git a/spec/support/shared_examples/requests/api/issues_shared_examples.rb b/spec/support/shared_examples/requests/api/issues_shared_examples.rb index d22210edf99..991dbced02d 100644 --- a/spec/support/shared_examples/requests/api/issues_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/issues_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_examples 'labeled issues with labels and label_name params' do +RSpec.shared_examples 'labeled issues with labels and label_name params' do shared_examples 'returns label names' do it 'returns label names' do expect_paginated_array_response(issue.id) diff --git a/spec/support/shared_examples/requests/api/logging_application_context_shared_examples.rb b/spec/support/shared_examples/requests/api/logging_application_context_shared_examples.rb new file mode 100644 index 00000000000..038ede884c8 --- /dev/null +++ b/spec/support/shared_examples/requests/api/logging_application_context_shared_examples.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'storing arguments in the application context' do + around do |example| + Labkit::Context.with_context { example.run } + end + + it 'places the expected params in the application context' do + # Stub the clearing of the context so we can validate it later + # The `around` block above makes sure we do clean it up later + allow(Labkit::Context).to receive(:pop) + + subject + + Labkit::Context.with_context do |context| + expect(context.to_h) + .to include(log_hash(expected_params)) + end + end + + def log_hash(hash) + hash.transform_keys! { |key| "meta.#{key}" } + end +end diff --git a/spec/support/shared_examples/requests/api/members_shared_examples.rb b/spec/support/shared_examples/requests/api/members_shared_examples.rb new file mode 100644 index 00000000000..fce75c29971 --- /dev/null +++ b/spec/support/shared_examples/requests/api/members_shared_examples.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a 404 response when source is private' do + before do + source.update_column(:visibility_level, Gitlab::VisibilityLevel::PRIVATE) + end + + it 'returns 404' do + route + + expect(response).to have_gitlab_http_status(:not_found) + end +end diff --git a/spec/support/shared_examples/requests/api/milestones_shared_examples.rb b/spec/support/shared_examples/requests/api/milestones_shared_examples.rb new file mode 100644 index 00000000000..b7cc5f2ca6b --- /dev/null +++ b/spec/support/shared_examples/requests/api/milestones_shared_examples.rb @@ -0,0 +1,405 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'group and project milestones' do |route_definition| + let(:resource_route) { "#{route}/#{milestone.id}" } + let(:label_1) { create(:label, title: 'label_1', project: project, priority: 1) } + let(:label_2) { create(:label, title: 'label_2', project: project, priority: 2) } + let(:label_3) { create(:label, title: 'label_3', project: project) } + let(:merge_request) { create(:merge_request, source_project: project) } + let(:another_merge_request) { create(:merge_request, :simple, source_project: project) } + + describe "GET #{route_definition}" do + it 'returns milestones list' do + get api(route, user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first['title']).to eq(milestone.title) + end + + it 'returns a 401 error if user not authenticated' do + get api(route) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + it 'returns an array of active milestones' do + get api("#{route}/?state=active", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(milestone.id) + end + + it 'returns an array of closed milestones' do + get api("#{route}/?state=closed", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(closed_milestone.id) + end + + it 'returns an array of milestones specified by iids' do + other_milestone = create(:milestone, project: try(:project), group: try(:group)) + + get api(route, user), params: { iids: [closed_milestone.iid, other_milestone.iid] } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.map { |m| m['id'] }).to match_array([closed_milestone.id, other_milestone.id]) + end + + it 'does not return any milestone if none found' do + get api(route, user), params: { iids: [Milestone.maximum(:iid).succ] } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + + it 'returns a milestone by iids array' do + get api("#{route}?iids=#{closed_milestone.iid}", user) + + expect(response.status).to eq 200 + expect(response).to include_pagination_headers + expect(json_response.size).to eq(1) + expect(json_response.size).to eq(1) + expect(json_response.first['title']).to eq closed_milestone.title + expect(json_response.first['id']).to eq closed_milestone.id + end + + it 'returns a milestone by title' do + get api(route, user), params: { title: 'version2' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.size).to eq(1) + expect(json_response.first['title']).to eq milestone.title + expect(json_response.first['id']).to eq milestone.id + end + + it 'returns a milestone by searching for title' do + get api(route, user), params: { search: 'version2' } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response.size).to eq(1) + expect(json_response.first['title']).to eq milestone.title + expect(json_response.first['id']).to eq milestone.id + end + + it 'returns a milestones by searching for description' do + get api(route, user), params: { search: 'open' } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response.size).to eq(1) + expect(json_response.first['title']).to eq milestone.title + expect(json_response.first['id']).to eq milestone.id + end + end + + describe "GET #{route_definition}/:milestone_id" do + it 'returns a milestone by id' do + get api(resource_route, user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['title']).to eq(milestone.title) + expect(json_response['iid']).to eq(milestone.iid) + end + + it 'returns 401 error if user not authenticated' do + get api(resource_route) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + it 'returns a 404 error if milestone id not found' do + get api("#{route}/1234", user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + describe "POST #{route_definition}" do + it 'creates a new milestone' do + post api(route, user), params: { title: 'new milestone' } + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['title']).to eq('new milestone') + expect(json_response['description']).to be_nil + end + + it 'creates a new milestone with description and dates' do + post api(route, user), params: { title: 'new milestone', description: 'release', due_date: '2013-03-02', start_date: '2013-02-02' } + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['description']).to eq('release') + expect(json_response['due_date']).to eq('2013-03-02') + expect(json_response['start_date']).to eq('2013-02-02') + end + + it 'returns a 400 error if title is missing' do + post api(route, user) + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'returns a 400 error if params are invalid (duplicate title)' do + post api(route, user), params: { title: milestone.title, description: 'release', due_date: '2013-03-02' } + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'creates a new milestone with reserved html characters' do + post api(route, user), params: { title: 'foo & bar 1.1 -> 2.2' } + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['title']).to eq('foo & bar 1.1 -> 2.2') + expect(json_response['description']).to be_nil + end + end + + describe "PUT #{route_definition}/:milestone_id" do + it 'updates a milestone' do + put api(resource_route, user), params: { title: 'updated title' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['title']).to eq('updated title') + end + + it 'removes a due date if nil is passed' do + milestone.update!(due_date: "2016-08-05") + + put api(resource_route, user), params: { due_date: nil } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['due_date']).to be_nil + end + + it 'returns a 404 error if milestone id not found' do + put api("#{route}/1234", user), params: { title: 'updated title' } + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'closes milestone' do + put api(resource_route, user), params: { state_event: 'close' } + expect(response).to have_gitlab_http_status(:ok) + + expect(json_response['state']).to eq('closed') + end + + it 'updates milestone with only start date' do + put api(resource_route, user), params: { start_date: Date.tomorrow } + + expect(response).to have_gitlab_http_status(:ok) + end + end + + describe "DELETE #{route_definition}/:milestone_id" do + it "rejects a member with reporter access from deleting a milestone" do + reporter = create(:user) + milestone.resource_parent.add_reporter(reporter) + + delete api(resource_route, reporter) + + expect(response).to have_gitlab_http_status(:forbidden) + end + + it 'deletes the milestone when the user has developer access to the project' do + delete api(resource_route, user) + + expect(project.milestones.find_by_id(milestone.id)).to be_nil + expect(response).to have_gitlab_http_status(:no_content) + end + end + + describe "GET #{route_definition}/:milestone_id/issues" do + let(:issues_route) { "#{route}/#{milestone.id}/issues" } + + before do + milestone.issues << create(:issue, project: project) + end + it 'returns issues for a particular milestone' do + get api(issues_route, user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first['milestone']['title']).to eq(milestone.title) + end + + it 'returns issues sorted by label priority' do + issue_1 = create(:labeled_issue, project: project, milestone: milestone, labels: [label_3]) + issue_2 = create(:labeled_issue, project: project, milestone: milestone, labels: [label_1]) + issue_3 = create(:labeled_issue, project: project, milestone: milestone, labels: [label_2]) + + get api(issues_route, user) + + expect(json_response.first['id']).to eq(issue_2.id) + expect(json_response.second['id']).to eq(issue_3.id) + expect(json_response.third['id']).to eq(issue_1.id) + end + + it 'matches V4 response schema for a list of issues' do + get api(issues_route, user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/issues') + end + + it 'returns a 401 error if user not authenticated' do + get api(issues_route) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + describe 'confidential issues' do + let!(:public_project) { create(:project, :public) } + let!(:context_group) { try(:group) } + let!(:milestone) do + context_group ? create(:milestone, group: context_group) : create(:milestone, project: public_project) + end + let!(:issue) { create(:issue, project: public_project) } + let!(:confidential_issue) { create(:issue, confidential: true, project: public_project) } + let!(:issues_route) do + if context_group + "#{route}/#{milestone.id}/issues" + else + "/projects/#{public_project.id}/milestones/#{milestone.id}/issues" + end + end + + before do + # Add public project to the group in context + setup_for_group if context_group + + public_project.add_developer(user) + milestone.issues << issue << confidential_issue + end + + it 'returns confidential issues to team members' do + get api(issues_route, user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + # 2 for projects, 3 for group(which has another project with an issue) + expect(json_response.size).to be_between(2, 3) + expect(json_response.map { |issue| issue['id'] }).to include(issue.id, confidential_issue.id) + end + + it 'does not return confidential issues to team members with guest role' do + member = create(:user) + public_project.add_guest(member) + + get api(issues_route, member) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + expect(json_response.map { |issue| issue['id'] }).to include(issue.id) + end + + it 'does not return confidential issues to regular users' do + get api(issues_route, create(:user)) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + expect(json_response.map { |issue| issue['id'] }).to include(issue.id) + end + + it 'returns issues ordered by label priority' do + issue.labels << label_2 + confidential_issue.labels << label_1 + + get api(issues_route, user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + # 2 for projects, 3 for group(which has another project with an issue) + expect(json_response.size).to be_between(2, 3) + expect(json_response.first['id']).to eq(confidential_issue.id) + expect(json_response.second['id']).to eq(issue.id) + end + end + end + + describe "GET #{route_definition}/:milestone_id/merge_requests" do + let(:merge_requests_route) { "#{route}/#{milestone.id}/merge_requests" } + + before do + milestone.merge_requests << merge_request + end + + it 'returns merge_requests for a particular milestone' do + # eager-load another_merge_request + another_merge_request + get api(merge_requests_route, user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + expect(json_response.first['title']).to eq(merge_request.title) + expect(json_response.first['milestone']['title']).to eq(milestone.title) + end + + it 'returns merge_requests sorted by label priority' do + merge_request_1 = create(:labeled_merge_request, source_branch: 'branch_1', source_project: project, milestone: milestone, labels: [label_2]) + merge_request_2 = create(:labeled_merge_request, source_branch: 'branch_2', source_project: project, milestone: milestone, labels: [label_1]) + merge_request_3 = create(:labeled_merge_request, source_branch: 'branch_3', source_project: project, milestone: milestone, labels: [label_3]) + + get api(merge_requests_route, user) + + expect(json_response.first['id']).to eq(merge_request_2.id) + expect(json_response.second['id']).to eq(merge_request_1.id) + expect(json_response.third['id']).to eq(merge_request_3.id) + end + + it 'returns a 404 error if milestone id not found' do + not_found_route = "#{route}/1234/merge_requests" + + get api(not_found_route, user) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns a 404 if the user has no access to the milestone' do + new_user = create :user + get api(merge_requests_route, new_user) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns a 401 error if user not authenticated' do + get api(merge_requests_route) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + it 'returns merge_requests ordered by position asc' do + milestone.merge_requests << another_merge_request + another_merge_request.labels << label_1 + merge_request.labels << label_2 + + get api(merge_requests_route, user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.size).to eq(2) + expect(json_response.first['id']).to eq(another_merge_request.id) + expect(json_response.second['id']).to eq(merge_request.id) + end + end +end diff --git a/spec/support/shared_examples/requests/api/notes.rb b/spec/support/shared_examples/requests/api/notes_shared_examples.rb index a793c23b809..bd38417a5db 100644 --- a/spec/support/shared_examples/requests/api/notes.rb +++ b/spec/support/shared_examples/requests/api/notes_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_examples 'noteable API' do |parent_type, noteable_type, id_name| +RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| describe "GET /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes" do context 'sorting' do before do diff --git a/spec/support/shared_examples/requests/api/pipelines/visibility_table_examples.rb b/spec/support/shared_examples/requests/api/pipelines/visibility_table_shared_examples.rb index dfd07176b1c..8dd2ef6ccc6 100644 --- a/spec/support/shared_examples/requests/api/pipelines/visibility_table_examples.rb +++ b/spec/support/shared_examples/requests/api/pipelines/visibility_table_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_examples 'pipelines visibility table' do +RSpec.shared_examples 'pipelines visibility table' do using RSpec::Parameterized::TableSyntax let(:ci_user) { create(:user) } diff --git a/spec/support/shared_examples/requests/api/read_user_shared_examples.rb b/spec/support/shared_examples/requests/api/read_user_shared_examples.rb new file mode 100644 index 00000000000..59cd0ab67b4 --- /dev/null +++ b/spec/support/shared_examples/requests/api/read_user_shared_examples.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'allows the "read_user" scope' do |api_version| + let(:version) { api_version || 'v4' } + + context 'for personal access tokens' do + context 'when the requesting token has the "api" scope' do + let(:token) { create(:personal_access_token, scopes: ['api'], user: user) } + + it 'returns a "200" response' do + get api_call.call(path, user, personal_access_token: token, version: version) + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'when the requesting token has the "read_user" scope' do + let(:token) { create(:personal_access_token, scopes: ['read_user'], user: user) } + + it 'returns a "200" response' do + get api_call.call(path, user, personal_access_token: token, version: version) + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'when the requesting token does not have any required scope' do + let(:token) { create(:personal_access_token, scopes: ['read_registry'], user: user) } + + before do + stub_container_registry_config(enabled: true) + end + + it 'returns a "403" response' do + get api_call.call(path, user, personal_access_token: token, version: version) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end + + context 'for doorkeeper (OAuth) tokens' do + let!(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) } + + context 'when the requesting token has the "api" scope' do + let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "api" } + + it 'returns a "200" response' do + get api_call.call(path, user, oauth_access_token: token) + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'when the requesting token has the "read_user" scope' do + let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "read_user" } + + it 'returns a "200" response' do + get api_call.call(path, user, oauth_access_token: token) + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'when the requesting token does not have any required scope' do + let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "invalid" } + + it 'returns a "403" response' do + get api_call.call(path, user, oauth_access_token: token) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end +end + +RSpec.shared_examples 'does not allow the "read_user" scope' do + context 'when the requesting token has the "read_user" scope' do + let(:token) { create(:personal_access_token, scopes: ['read_user'], user: user) } + + it 'returns a "403" response' do + post api_call.call(path, user, personal_access_token: token), params: attributes_for(:user, projects_limit: 3) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end +end diff --git a/spec/support/shared_examples/requests/api/repositories_shared_context.rb b/spec/support/shared_examples/requests/api/repositories_shared_context.rb new file mode 100644 index 00000000000..cc3a495bec1 --- /dev/null +++ b/spec/support/shared_examples/requests/api/repositories_shared_context.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +RSpec.shared_context 'disabled repository' do + before do + project.project_feature.update!( + repository_access_level: ProjectFeature::DISABLED, + merge_requests_access_level: ProjectFeature::DISABLED, + builds_access_level: ProjectFeature::DISABLED + ) + expect(project.feature_available?(:repository, current_user)).to be false + end +end diff --git a/spec/support/shared_examples/requests/api/resolvable_discussions.rb b/spec/support/shared_examples/requests/api/resolvable_discussions_shared_examples.rb index dd764cf2d4d..8d2a3f13d8e 100644 --- a/spec/support/shared_examples/requests/api/resolvable_discussions.rb +++ b/spec/support/shared_examples/requests/api/resolvable_discussions_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_examples 'resolvable discussions API' do |parent_type, noteable_type, id_name| +RSpec.shared_examples 'resolvable discussions API' do |parent_type, noteable_type, id_name| describe "PUT /#{parent_type}/:id/#{noteable_type}/:noteable_id/discussions/:discussion_id" do it "resolves discussion if resolved is true" do put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\ diff --git a/spec/support/shared_examples/requests/api/resource_label_events_api_shared_examples.rb b/spec/support/shared_examples/requests/api/resource_label_events_api_shared_examples.rb new file mode 100644 index 00000000000..520c3ea8e47 --- /dev/null +++ b/spec/support/shared_examples/requests/api/resource_label_events_api_shared_examples.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'resource_label_events API' do |parent_type, eventable_type, id_name| + describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_label_events" do + context "with local label reference" do + let!(:event) { create_event(label) } + + it "returns an array of resource label events" do + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_label_events", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first['id']).to eq(event.id) + end + + it "returns a 404 error when eventable id not found" do + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/12345/resource_label_events", user) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it "returns 404 when not authorized" do + parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + private_user = create(:user) + + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_label_events", private_user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context "with cross-project label reference" do + let(:private_project) { create(:project, :private) } + let(:project_label) { create(:label, project: private_project) } + let!(:event) { create_event(project_label) } + + it "returns cross references accessible by user" do + private_project.add_guest(user) + + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_label_events", user) + + expect(json_response).to be_an Array + expect(json_response.first['id']).to eq(event.id) + end + + it "does not return cross references not accessible by user" do + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_label_events", user) + + expect(json_response).to be_an Array + expect(json_response).to eq [] + end + end + end + + describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_label_events/:event_id" do + context "with local label reference" do + let!(:event) { create_event(label) } + + it "returns a resource label event by id" do + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_label_events/#{event.id}", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['id']).to eq(event.id) + end + + it "returns 404 when not authorized" do + parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + private_user = create(:user) + + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_label_events/#{event.id}", private_user) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it "returns a 404 error if resource label event not found" do + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_label_events/12345", user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context "with cross-project label reference" do + let(:private_project) { create(:project, :private) } + let(:project_label) { create(:label, project: private_project) } + let!(:event) { create_event(project_label) } + + it "returns a 404 error if cross-reference project is not accessible" do + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_label_events/#{event.id}", user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + def create_event(label) + create(:resource_label_event, eventable.class.name.underscore => eventable, label: label) + end +end diff --git a/spec/support/shared_examples/requests/api/status_shared_examples.rb b/spec/support/shared_examples/requests/api/status_shared_examples.rb index d5845863a58..8207190b1dc 100644 --- a/spec/support/shared_examples/requests/api/status_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/status_shared_examples.rb @@ -4,7 +4,7 @@ # # Requires an API request: # let(:request) { get api("/projects/#{project.id}/repository/branches", user) } -shared_examples_for '400 response' do +RSpec.shared_examples '400 response' do let(:message) { nil } before do @@ -21,7 +21,7 @@ shared_examples_for '400 response' do end end -shared_examples_for '403 response' do +RSpec.shared_examples '403 response' do before do # Fires the request request @@ -32,7 +32,7 @@ shared_examples_for '403 response' do end end -shared_examples_for '404 response' do +RSpec.shared_examples '404 response' do let(:message) { nil } before do @@ -50,7 +50,7 @@ shared_examples_for '404 response' do end end -shared_examples_for '412 response' do +RSpec.shared_examples '412 response' do let(:params) { nil } let(:success_status) { 204 } diff --git a/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb b/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb new file mode 100644 index 00000000000..30ba8d9b436 --- /dev/null +++ b/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb @@ -0,0 +1,148 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'an unauthorized API user' do + it { is_expected.to eq(403) } +end + +RSpec.shared_examples 'time tracking endpoints' do |issuable_name| + let(:non_member) { create(:user) } + + issuable_collection_name = issuable_name.pluralize + + describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_estimate" do + context 'with an unauthorized user' do + subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", non_member), params: { duration: '1w' }) } + + it_behaves_like 'an unauthorized API user' + end + + it "sets the time estimate for #{issuable_name}" do + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", user), params: { duration: '1w' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['human_time_estimate']).to eq('1w') + end + + describe 'updating the current estimate' do + before do + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", user), params: { duration: '1w' } + end + + context 'when duration has a bad format' do + it 'does not modify the original estimate' do + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", user), params: { duration: 'foo' } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(issuable.reload.human_time_estimate).to eq('1w') + end + end + + context 'with a valid duration' do + it 'updates the estimate' do + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", user), params: { duration: '3w1h' } + + expect(response).to have_gitlab_http_status(:ok) + expect(issuable.reload.human_time_estimate).to eq('3w 1h') + end + end + end + end + + describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/reset_time_estimate" do + context 'with an unauthorized user' do + subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/reset_time_estimate", non_member)) } + + it_behaves_like 'an unauthorized API user' + end + + it "resets the time estimate for #{issuable_name}" do + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/reset_time_estimate", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['time_estimate']).to eq(0) + end + end + + describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/add_spent_time" do + context 'with an unauthorized user' do + subject do + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", non_member), params: { duration: '2h' } + end + + it_behaves_like 'an unauthorized API user' + end + + it "add spent time for #{issuable_name}" do + Timecop.travel(1.minute.from_now) do + expect do + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user), params: { duration: '2h' } + end.to change { issuable.reload.updated_at } + end + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['human_total_time_spent']).to eq('2h') + end + + context 'when subtracting time' do + it 'subtracts time of the total spent time' do + Timecop.travel(1.minute.from_now) do + expect do + issuable.update!(spend_time: { duration: 7200, user_id: user.id }) + end.to change { issuable.reload.updated_at } + end + + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user), params: { duration: '-1h' } + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['total_time_spent']).to eq(3600) + end + end + + context 'when time to subtract is greater than the total spent time' do + it 'does not modify the total time spent' do + issuable.update!(spend_time: { duration: 7200, user_id: user.id }) + + Timecop.travel(1.minute.from_now) do + expect do + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user), params: { duration: '-1w' } + end.not_to change { issuable.reload.updated_at } + end + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']['time_spent'].first).to match(/exceeds the total time spent/) + end + end + end + + describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/reset_spent_time" do + context 'with an unauthorized user' do + subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/reset_spent_time", non_member)) } + + it_behaves_like 'an unauthorized API user' + end + + it "resets spent time for #{issuable_name}" do + Timecop.travel(1.minute.from_now) do + expect do + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/reset_spent_time", user) + end.to change { issuable.reload.updated_at } + end + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['total_time_spent']).to eq(0) + end + end + + describe "GET /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_stats" do + it "returns the time stats for #{issuable_name}" do + issuable.update!(spend_time: { duration: 1800, user_id: user.id }, + time_estimate: 3600) + + get api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_stats", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['total_time_spent']).to eq(1800) + expect(json_response['time_estimate']).to eq(3600) + end + end +end |