Commit d6f55cb6 authored by Jarka Kadlecová's avatar Jarka Kadlecová

Add api for epic_issue associations

parent 627a8882
---
title: Add api for epic_issue associations
merge_request:
author:
type: added
......@@ -18,6 +18,7 @@ following locations:
- [Deployments](deployments.md)
- [Deploy Keys](deploy_keys.md)
- [Environments](environments.md)
- [Epic Issues](epic_issues.md)
- [Events](events.md)
- [Feature flags](features.md)
- [Geo Nodes](geo_nodes.md)
......
This diff is collapsed.
module API
class EpicIssues < Grape::API
before do
authenticate!
authorize_epics_feature!
end
helpers do
def authorize_epics_feature!
forbidden! unless user_group.feature_available?(:epics)
end
def authorize_can_read!
authorize!(:read_epic, epic)
end
def authorize_can_admin!
authorize!(:admin_epic, epic)
end
def epic
@epic ||= user_group.epics.find_by(iid: params[:epic_iid])
end
def link
@link ||= epic.epic_issues.find(params[:epic_issue_id])
end
end
params do
requires :id, type: String, desc: 'The ID of a group'
end
resource :groups, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Get issues assigned to the epic' do
success Entities::EpicIssue
end
params do
requires :epic_iid, type: Integer, desc: 'The iid of the epic'
end
get ':id/-/epics/:epic_iid/issues' do
authorize_can_read!
present epic.issues(current_user),
with: Entities::EpicIssue,
current_user: current_user
end
desc 'Assign an issue to the epic' do
success Entities::EpicIssueLink
end
params do
requires :epic_iid, type: Integer, desc: 'The iid of the epic'
end
post ':id/-/epics/:epic_iid/issues/:issue_id' do
authorize_can_admin!
issue = Issue.find(params[:issue_id])
create_params = { target_issue: issue }
result = ::EpicIssues::CreateService.new(epic, current_user, create_params).execute
if result[:status] == :success
epic_issue_link = EpicIssue.find_by!(epic: epic, issue: issue)
present epic_issue_link, with: Entities::EpicIssueLink
else
render_api_error!(result[:message], result[:http_status])
end
end
desc 'Remove an issue from the epic' do
success Entities::EpicIssueLink
end
params do
requires :epic_iid, type: Integer, desc: 'The iid of the epic'
requires :epic_issue_id, type: Integer, desc: 'The id of the association'
end
delete ':id/-/epics/:epic_iid/issues/:epic_issue_id' do
authorize_can_admin!
result = ::EpicIssues::DestroyService.new(link, current_user).execute
if result[:status] == :success
present link, with: Entities::EpicIssueLink
else
render_api_error!(result[:message], result[:http_status])
end
end
end
end
end
......@@ -124,6 +124,7 @@ module API
mount ::API::DeployKeys
mount ::API::Deployments
mount ::API::Environments
mount ::API::EpicIssues
mount ::API::Events
mount ::API::Features
mount ::API::Files
......
......@@ -491,6 +491,26 @@ module API
expose :issue_link_id
end
class Epic < Grape::Entity
expose :id
expose :iid
expose :title
expose :description
expose :author, using: Entities::UserBasic
expose :start_date
expose :end_date
end
class EpicIssue < Issue
expose :epic_issue_id
end
class EpicIssueLink < Grape::Entity
expose :id
expose :epic, using: Entities::Epic
expose :issue, using: Entities::IssueBasic
end
class IssueLink < Grape::Entity
expose :source, as: :source_issue, using: Entities::IssueBasic
expose :target, as: :target_issue, using: Entities::IssueBasic
......
{
"type": "object",
"properties" : {
"id": { "type": "integer" },
"issue": { "type": "object" },
"epic": {
"type": "object",
"required": [
"id",
"iid",
"title"
],
"properties": {
"id": { "type": "integer" },
"iid": { "type": "integer" },
"title": { "type": "string" },
"description": { "type": ["string", "null"] },
"author": { "type": ["object", "null"] },
"start_date": { "type": ["string", "null"] },
"end_date": { "type": ["string", "null"] }
},
"additionalProperties": false
},
"issue": {
"allOf": [
{ "$ref": "../../../../../../fixtures/api/schemas/public_api/v4/issue.json" },
{
"properties": {
"weight": { "type": ["integer", "null"] }
}
}
]
}
},
"required" : [ "id", "epic", "issue" ],
"additionalProperties": false
}
{
"allOf": [
{ "$ref": "./issues.json" },
{
"properties": {
"issue_link_id": { "type": ["integer", "null"] },
"position": { "type": ["integer", "null"] }
}
}
]
}
require 'spec_helper'
describe API::EpicIssues do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, :public, group: group) }
let(:epic) { create(:epic, group: group) }
describe 'GET /groups/:id/-/epics/:epic_iid/issues' do
let(:url) { "/groups/#{group.path}/-/epics/#{epic.iid}/issues" }
context 'when epics feature is disabled' do
it 'returns 403 forbidden error' do
group.add_developer(user)
get api(url, user)
expect(response).to have_gitlab_http_status(403)
end
end
context 'when epics feature is enabled' do
before do
stub_licensed_features(epics: true)
end
context 'when an error occurs' do
it 'returns 401 unauthorized error for non authenticated user' do
get api(url)
expect(response).to have_gitlab_http_status(401)
end
it 'returns 404 not found error for a user without permissions to see the group' do
project.update(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
group.update(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
get api(url, user)
expect(response).to have_gitlab_http_status(404)
end
end
context 'when the request is correct' do
let(:issues) { create_list(:issue, 2, project: project) }
let!(:epic_issue1) { create(:epic_issue, epic: epic, issue: issues[0]) }
let!(:epic_issue2) { create(:epic_issue, epic: epic, issue: issues[1]) }
before do
get api(url, user)
end
it 'returns 200 status' do
expect(response).to have_gitlab_http_status(200)
end
it 'matches the response schema' do
expect(response).to match_response_schema('public_api/v4/epic_issues', dir: 'ee')
end
end
end
end
describe 'POST /groups/:id/-/epics/:epic_iid/issues' do
let(:issue) { create(:issue, project: project) }
let(:url) { "/groups/#{group.path}/-/epics/#{epic.iid}/issues/#{issue.id}" }
context 'when epics feature is disabled' do
it 'returns 403 forbidden error' do
group.add_developer(user)
post api(url, user)
expect(response).to have_gitlab_http_status(403)
end
end
context 'when epics feature is enabled' do
before do
stub_licensed_features(epics: true)
end
context 'when an error occurs' do
it 'returns 401 unauthorized error for non authenticated user' do
post api(url)
expect(response).to have_gitlab_http_status(401)
end
it 'returns 404 not found error for a user without permissions to see the group' do
project.update(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
group.update(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
post api(url, user)
expect(response).to have_gitlab_http_status(404)
end
it 'returns 403 forbidden error for a user without permissions to admin the epic' do
post api(url, user)
expect(response).to have_gitlab_http_status(403)
end
context 'when issue project is not under the epic group' do
before do
other_project = create(:project)
issue.update_attribute(:project, other_project)
group.add_developer(user)
other_project.add_developer(user)
end
it 'returns an error' do
post api(url, user)
expect(response).to have_gitlab_http_status(404)
expect(json_response).to eq('message' => 'No Issue found for given params')
end
end
end
context 'when the request is correct' do
before do
group.add_developer(user)
post api(url, user)
end
it 'returns 201 status' do
expect(response).to have_gitlab_http_status(201)
end
it 'matches the response schema' do
expect(response).to match_response_schema('public_api/v4/epic_issue_link', dir: 'ee')
end
it 'assigns the issue to the epic' do
epic_issue = EpicIssue.last
expect(epic_issue.issue).to eq(issue)
expect(epic_issue.epic).to eq(epic)
end
end
end
end
describe 'DELETE /groups/:id/-/epics/:epic_iid/issues/:epic_issue_id"' do
let(:issue) { create(:issue, project: project) }
let!(:epic_issue) { create(:epic_issue, epic: epic, issue: issue) }
let(:url) { "/groups/#{group.path}/-/epics/#{epic.iid}/issues/#{epic_issue.id}" }
context 'when epics feature is disabled' do
it 'returns 403 forbidden error' do
group.add_developer(user)
post api(url, user)
expect(response).to have_gitlab_http_status(403)
end
end
context 'when epics feature is enabled' do
before do
stub_licensed_features(epics: true)
end
context 'when an error occurs' do
it 'returns 401 unauthorized error for non authenticated user' do
delete api(url)
expect(response).to have_gitlab_http_status(401)
end
it 'returns 404 not found error for a user without permissions to see the group' do
project.update(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
group.update(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
delete api(url, user)
expect(response).to have_gitlab_http_status(404)
end
it 'returns 403 forbidden error for a user without permissions to admin the epic' do
delete api(url, user)
expect(response).to have_gitlab_http_status(403)
end
context 'when epic_issue association does not include the epic in the url' do
before do
other_group = create(:group)
other_group_epic = create(:epic, group: other_group)
epic_issue.update_attribute(:epic, other_group_epic)
group.add_developer(user)
other_group.add_developer(user)
end
it 'returns 404 not found error' do
delete api(url, user)
expect(response).to have_gitlab_http_status(404)
end
end
end
context 'when the request is correct' do
before do
group.add_developer(user)
end
it 'returns 200 status' do
delete api(url, user)
expect(response).to have_gitlab_http_status(200)
end
it 'matches the response schema' do
delete api(url, user)
expect(response).to match_response_schema('public_api/v4/epic_issue_link', dir: 'ee')
end
it 'removes the association' do
expect { delete api(url, user) }.to change { EpicIssue.count }.from(1).to(0)
end
end
end
end
end
{
"type": "object",
"properties" : {
"id": { "type": "integer" },
"iid": { "type": "integer" },
"project_id": { "type": "integer" },
"title": { "type": "string" },
"description": { "type": ["string", "null"] },
"state": { "type": "string" },
"discussion_locked": { "type": ["boolean", "null"] },
"closed_at": { "type": "date" },
"created_at": { "type": "date" },
"updated_at": { "type": "date" },
"labels": {
"type": "array",
"items": {
"type": "string"
}
},
"milestone": {
"type": ["object", "null"],
"properties": {
"id": { "type": "integer" },
"iid": { "type": "integer" },
"project_id": { "type": ["integer", "null"] },
"group_id": { "type": ["integer", "null"] },
"title": { "type": "string" },
"description": { "type": ["string", "null"] },
"state": { "type": "string" },
"created_at": { "type": "date" },
"updated_at": { "type": "date" },
"due_date": { "type": "date" },
"start_date": { "type": "date" }
},
"additionalProperties": false
},
"assignees": {
"type": "array",
"items": {
"type": ["object", "null"],
"properties": {
"name": { "type": "string" },
"username": { "type": "string" },
"id": { "type": "integer" },
"state": { "type": "string" },
"avatar_url": { "type": "uri" },
"web_url": { "type": "uri" }
},
"additionalProperties": false
}
},
"assignee": {
"type": ["object", "null"],
"properties": {
"name": { "type": "string" },
"username": { "type": "string" },
"id": { "type": "integer" },
"state": { "type": "string" },
"avatar_url": { "type": "uri" },
"web_url": { "type": "uri" }
},
"additionalProperties": false
},
"author": {
"type": "object",
"properties": {
"name": { "type": "string" },
"username": { "type": "string" },
"id": { "type": "integer" },
"state": { "type": "string" },
"avatar_url": { "type": "uri" },
"web_url": { "type": "uri" }
},
"additionalProperties": false
},
"user_notes_count": { "type": "integer" },
"upvotes": { "type": "integer" },
"downvotes": { "type": "integer" },
"due_date": { "type": ["date", "null"] },
"confidential": { "type": "boolean" },
"web_url": { "type": "uri" },
"time_stats": {
"time_estimate": { "type": "integer" },
"total_time_spent": { "type": "integer" },
"human_time_estimate": { "type": ["string", "null"] },
"human_total_time_spent": { "type": ["string", "null"] }
}
},
"required": [
"id", "iid", "project_id", "title", "description",
"state", "created_at", "updated_at", "labels",
"milestone", "assignees", "author", "user_notes_count",
"upvotes", "downvotes", "due_date", "confidential",
"web_url"
]
}
......@@ -3,97 +3,7 @@
"items": {
"type": "object",
"properties" : {
"id": { "type": "integer" },
"iid": { "type": "integer" },
"project_id": { "type": "integer" },
"title": { "type": "string" },
"description": { "type": ["string", "null"] },
"state": { "type": "string" },
"discussion_locked": { "type": ["boolean", "null"] },
"closed_at": { "type": "date" },
"created_at": { "type": "date" },
"updated_at": { "type": "date" },
"labels": {
"type": "array",
"items": {
"type": "string"
}
},
"milestone": {
"type": "object",
"properties": {
"id": { "type": "integer" },
"iid": { "type": "integer" },
"project_id": { "type": ["integer", "null"] },
"group_id": { "type": ["integer", "null"] },
"title": { "type": "string" },
"description": { "type": ["string", "null"] },
"state": { "type": "string" },
"created_at": { "type": "date" },
"updated_at": { "type": "date" },
"due_date": { "type": "date" },
"start_date": { "type": "date" }
},
"additionalProperties": false
},
"assignees": {
"type": "array",
"items": {
"type": ["object", "null"],
"properties": {
"name": { "type": "string" },
"username": { "type": "string" },
"id": { "type": "integer" },
"state": { "type": "string" },
"avatar_url": { "type": "uri" },
"web_url": { "type": "uri" }
},
"additionalProperties": false
}
},
"assignee": {
"type": ["object", "null"],
"properties": {
"name": { "type": "string" },
"username": { "type": "string" },
"id": { "type": "integer" },
"state": { "type": "string" },
"avatar_url": { "type": "uri" },
"web_url": { "type": "uri" }
},
"additionalProperties": false
},
"author": {
"type": "object",
"properties": {
"name": { "type": "string" },
"username": { "type": "string" },
"id": { "type": "integer" },
"state": { "type": "string" },
"avatar_url": { "type": "uri" },
"web_url": { "type": "uri" }
},
"additionalProperties": false
},
"user_notes_count": { "type": "integer" },
"upvotes": { "type": "integer" },
"downvotes": { "type": "integer" },
"due_date": { "type": ["date", "null"] },
"confidential": { "type": "boolean" },
"web_url": { "type": "uri" },
"time_stats": {
"time_estimate": { "type": "integer" },
"total_time_spent": { "type": "integer" },
"human_time_estimate": { "type": ["string", "null"] },
"human_total_time_spent": { "type": ["string", "null"] }
}
},
"required": [
"id", "iid", "project_id", "title", "description",
"state", "created_at", "updated_at", "labels",
"milestone", "assignees", "author", "user_notes_count",
"upvotes", "downvotes", "due_date", "confidential",
"web_url"
]
"$ref": "./issue.json"
}
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment