summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFelipe Artur <felipefac@gmail.com>2017-12-06 17:07:47 -0200
committerFelipe Artur <felipefac@gmail.com>2018-01-04 17:28:10 -0200
commit5e148d4e931792733400f59864e1aa886ef55953 (patch)
tree3e08b1776015895dcb242909df01f5db60265d7e
parente5a9b9a14d32d890dea20403c977dfd569eb3e17 (diff)
downloadgitlab-ce-5e148d4e931792733400f59864e1aa886ef55953.tar.gz
EE-BACKPORT group boards
-rw-r--r--app/finders/labels_finder.rb19
-rw-r--r--app/policies/group_policy.rb8
-rw-r--r--doc/api/boards.md200
-rw-r--r--lib/api/api.rb4
-rw-r--r--lib/api/boards.rb84
-rw-r--r--lib/api/boards_responses.rb50
-rw-r--r--lib/api/entities.rb2
-rw-r--r--lib/api/helpers.rb15
-rw-r--r--lib/api/labels.rb4
-rw-r--r--lib/api/v3/labels.rb2
-rw-r--r--spec/finders/labels_finder_spec.rb10
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/board.json86
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/boards.json4
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/user/basic.json2
-rw-r--r--spec/requests/api/boards_spec.rb179
-rw-r--r--spec/support/api/boards_shared_examples.rb180
16 files changed, 601 insertions, 248 deletions
diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb
index ce432ddbfe6..6de9eb89468 100644
--- a/app/finders/labels_finder.rb
+++ b/app/finders/labels_finder.rb
@@ -1,4 +1,6 @@
class LabelsFinder < UnionFinder
+ include Gitlab::Utils::StrongMemoize
+
def initialize(current_user, params = {})
@current_user = current_user
@params = params
@@ -32,6 +34,8 @@ class LabelsFinder < UnionFinder
label_ids << project.labels
end
end
+ elsif only_group_labels?
+ label_ids << Label.where(group_id: group.id)
else
label_ids << Label.where(group_id: projects.group_ids)
label_ids << Label.where(project_id: projects.select(:id))
@@ -51,6 +55,13 @@ class LabelsFinder < UnionFinder
items.where(title: title)
end
+ def group
+ strong_memoize(:group) do
+ group = Group.find(params[:group_id])
+ authorized_to_read_labels?(group) && group
+ end
+ end
+
def group?
params[:group_id].present?
end
@@ -63,6 +74,10 @@ class LabelsFinder < UnionFinder
params[:project_ids].present?
end
+ def only_group_labels?
+ params[:only_group_labels]
+ end
+
def title
params[:title] || params[:name]
end
@@ -96,9 +111,9 @@ class LabelsFinder < UnionFinder
@projects
end
- def authorized_to_read_labels?(project)
+ def authorized_to_read_labels?(label_parent)
return true if skip_authorization
- Ability.allowed?(current_user, :read_label, project)
+ Ability.allowed?(current_user, :read_label, label_parent)
end
end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index d2d45e402b0..f0bcba588a2 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -28,12 +28,18 @@ class GroupPolicy < BasePolicy
with_options scope: :subject, score: 0
condition(:request_access_enabled) { @subject.request_access_enabled }
- rule { public_group } .enable :read_group
+ rule { public_group }.policy do
+ enable :read_group
+ enable :read_list
+ enable :read_label
+ end
+
rule { logged_in_viewable }.enable :read_group
rule { guest }.policy do
enable :read_group
enable :upload_file
+ enable :read_label
end
rule { admin } .enable :read_group
diff --git a/doc/api/boards.md b/doc/api/boards.md
index 69c47abc806..a5f455e1c43 100644
--- a/doc/api/boards.md
+++ b/doc/api/boards.md
@@ -15,10 +15,10 @@ GET /projects/:id/boards
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/boards
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards
```
Example response:
@@ -27,6 +27,19 @@ Example response:
[
{
"id" : 1,
+ "project": {
+ "id": 5,
+ "name": "Diaspora Project Site",
+ "name_with_namespace": "Diaspora / Diaspora Project Site",
+ "path": "diaspora-project-site",
+ "path_with_namespace": "diaspora/diaspora-project-site",
+ "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
+ "web_url": "http://example.com/diaspora/diaspora-project-site"
+ },
+ "milestone": {
+ "id": 12
+ "title": "10.0"
+ },
"lists" : [
{
"id" : 1,
@@ -60,6 +73,159 @@ Example response:
]
```
+## Single board
+
+Get a single board.
+
+```
+GET /projects/:id/boards/:board_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `board_id` | integer | yes | The ID of a board |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1
+```
+
+Example response:
+
+```json
+ {
+ "id": 1,
+ "name:": "project issue board",
+ "project": {
+ "id": 5,
+ "name": "Diaspora Project Site",
+ "name_with_namespace": "Diaspora / Diaspora Project Site",
+ "path": "diaspora-project-site",
+ "path_with_namespace": "diaspora/diaspora-project-site",
+ "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
+ "web_url": "http://example.com/diaspora/diaspora-project-site"
+ },
+ "milestone": {
+ "id": 12
+ "title": "10.0"
+ },
+ "lists" : [
+ {
+ "id" : 1,
+ "label" : {
+ "name" : "Testing",
+ "color" : "#F0AD4E",
+ "description" : null
+ },
+ "position" : 1
+ },
+ {
+ "id" : 2,
+ "label" : {
+ "name" : "Ready",
+ "color" : "#FF0000",
+ "description" : null
+ },
+ "position" : 2
+ },
+ {
+ "id" : 3,
+ "label" : {
+ "name" : "Production",
+ "color" : "#FF5F00",
+ "description" : null
+ },
+ "position" : 3
+ }
+ ]
+ }
+```
+
+## Create a board (EES-Only)
+
+Creates a board.
+
+```
+POST /projects/:id/boards
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `name` | string | yes | The name of the new board |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards?name=newboard
+```
+
+Example response:
+
+```json
+ {
+ "id": 1,
+ "project": {
+ "id": 5,
+ "name": "Diaspora Project Site",
+ "name_with_namespace": "Diaspora / Diaspora Project Site",
+ "path": "diaspora-project-site",
+ "path_with_namespace": "diaspora/diaspora-project-site",
+ "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
+ "web_url": "http://example.com/diaspora/diaspora-project-site"
+ },
+ "name": "newboard",
+ "milestone": {
+ "id": 12
+ "title": "10.0"
+ },
+ "lists" : [
+ {
+ "id" : 1,
+ "label" : {
+ "name" : "Testing",
+ "color" : "#F0AD4E",
+ "description" : null
+ },
+ "position" : 1
+ },
+ {
+ "id" : 2,
+ "label" : {
+ "name" : "Ready",
+ "color" : "#FF0000",
+ "description" : null
+ },
+ "position" : 2
+ },
+ {
+ "id" : 3,
+ "label" : {
+ "name" : "Production",
+ "color" : "#FF5F00",
+ "description" : null
+ },
+ "position" : 3
+ }
+ ]
+ }
+```
+
+## Delete a board (EES-Only)
+
+Deletes a board.
+
+```
+DELETE /projects/:id/boards/:board_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `board_id` | integer | yes | The ID of a board |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1
+```
+
## List board lists
Get a list of the board's lists.
@@ -71,8 +237,8 @@ GET /projects/:id/boards/:board_id/lists
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `board_id` | integer | yes | The ID of a board |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `board_id` | integer | yes | The ID of a board |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists
@@ -122,9 +288,9 @@ GET /projects/:id/boards/:board_id/lists/:list_id
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `board_id` | integer | yes | The ID of a board |
-| `list_id`| integer | yes | The ID of a board's list |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `board_id` | integer | yes | The ID of a board |
+| `list_id`| integer | yes | The ID of a board's list |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists/1
@@ -154,9 +320,9 @@ POST /projects/:id/boards/:board_id/lists
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `board_id` | integer | yes | The ID of a board |
-| `label_id` | integer | yes | The ID of a label |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `board_id` | integer | yes | The ID of a board |
+| `label_id` | integer | yes | The ID of a label |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists?label_id=5
@@ -186,10 +352,10 @@ PUT /projects/:id/boards/:board_id/lists/:list_id
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `board_id` | integer | yes | The ID of a board |
-| `list_id` | integer | yes | The ID of a board's list |
-| `position` | integer | yes | The position of the list |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `board_id` | integer | yes | The ID of a board |
+| `list_id` | integer | yes | The ID of a board's list |
+| `position` | integer | yes | The position of the list |
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists/1?position=2
@@ -219,9 +385,9 @@ DELETE /projects/:id/boards/:board_id/lists/:list_id
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `board_id` | integer | yes | The ID of a board |
-| `list_id` | integer | yes | The ID of a board's list |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `board_id` | integer | yes | The ID of a board |
+| `list_id` | integer | yes | The ID of a board's list |
```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists/1
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 8094597d238..e0d14281c96 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -119,6 +119,7 @@ module API
mount ::API::Features
mount ::API::Files
mount ::API::Groups
+ mount ::API::GroupMilestones
mount ::API::Internal
mount ::API::Issues
mount ::API::Jobs
@@ -129,8 +130,6 @@ module API
mount ::API::Members
mount ::API::MergeRequestDiffs
mount ::API::MergeRequests
- mount ::API::ProjectMilestones
- mount ::API::GroupMilestones
mount ::API::Namespaces
mount ::API::Notes
mount ::API::NotificationSettings
@@ -139,6 +138,7 @@ module API
mount ::API::PipelineSchedules
mount ::API::ProjectHooks
mount ::API::Projects
+ mount ::API::ProjectMilestones
mount ::API::ProjectSnippets
mount ::API::ProtectedBranches
mount ::API::Repositories
diff --git a/lib/api/boards.rb b/lib/api/boards.rb
index 366b0dc9a6f..6c706b2b4e1 100644
--- a/lib/api/boards.rb
+++ b/lib/api/boards.rb
@@ -1,45 +1,46 @@
module API
class Boards < Grape::API
+ include BoardsResponses
include PaginationParams
before { authenticate! }
+ helpers do
+ def board_parent
+ user_project
+ end
+ end
+
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
- desc 'Get all project boards' do
- detail 'This feature was introduced in 8.13'
- success Entities::Board
- end
- params do
- use :pagination
- end
- get ':id/boards' do
- authorize!(:read_board, user_project)
- present paginate(user_project.boards), with: Entities::Board
+ segment ':id/boards' do
+ desc 'Get all project boards' do
+ detail 'This feature was introduced in 8.13'
+ success Entities::Board
+ end
+ params do
+ use :pagination
+ end
+ get '/' do
+ authorize!(:read_board, user_project)
+ present paginate(board_parent.boards), with: Entities::Board
+ end
+
+ desc 'Find a project board' do
+ detail 'This feature was introduced in 10.4'
+ success Entities::Board
+ end
+ get '/:board_id' do
+ present board, with: Entities::Board
+ end
end
params do
requires :board_id, type: Integer, desc: 'The ID of a board'
end
segment ':id/boards/:board_id' do
- helpers do
- def project_board
- board = user_project.boards.first
-
- if params[:board_id] == board.id
- board
- else
- not_found!('Board')
- end
- end
-
- def board_lists
- project_board.lists.destroyable
- end
- end
-
desc 'Get the lists of a project board' do
detail 'Does not include `done` list. This feature was introduced in 8.13'
success Entities::List
@@ -72,22 +73,13 @@ module API
requires :label_id, type: Integer, desc: 'The ID of an existing label'
end
post '/lists' do
- unless available_labels.exists?(params[:label_id])
+ unless available_labels_for(user_project).exists?(params[:label_id])
render_api_error!({ error: 'Label not found!' }, 400)
end
authorize!(:admin_list, user_project)
- service = ::Boards::Lists::CreateService.new(user_project, current_user,
- { label_id: params[:label_id] })
-
- list = service.execute(project_board)
-
- if list.valid?
- present list, with: Entities::List
- else
- render_validation_error!(list)
- end
+ create_list
end
desc 'Moves a board list to a new position' do
@@ -99,18 +91,11 @@ module API
requires :position, type: Integer, desc: 'The position of the list'
end
put '/lists/:list_id' do
- list = project_board.lists.movable.find(params[:list_id])
+ list = board_lists.find(params[:list_id])
authorize!(:admin_list, user_project)
- service = ::Boards::Lists::MoveService.new(user_project, current_user,
- { position: params[:position] })
-
- if service.execute(list)
- present list, with: Entities::List
- else
- render_api_error!({ error: "List could not be moved!" }, 400)
- end
+ move_list(list)
end
desc 'Delete a board list' do
@@ -124,12 +109,7 @@ module API
authorize!(:admin_list, user_project)
list = board_lists.find(params[:list_id])
- destroy_conditionally!(list) do |list|
- service = ::Boards::Lists::DestroyService.new(user_project, current_user)
- unless service.execute(list)
- render_api_error!({ error: 'List could not be deleted!' }, 400)
- end
- end
+ destroy_list(list)
end
end
end
diff --git a/lib/api/boards_responses.rb b/lib/api/boards_responses.rb
new file mode 100644
index 00000000000..ead0943a74d
--- /dev/null
+++ b/lib/api/boards_responses.rb
@@ -0,0 +1,50 @@
+module API
+ module BoardsResponses
+ extend ActiveSupport::Concern
+
+ included do
+ helpers do
+ def board
+ board_parent.boards.find(params[:board_id])
+ end
+
+ def board_lists
+ board.lists.destroyable
+ end
+
+ def create_list
+ create_list_service =
+ ::Boards::Lists::CreateService.new(board_parent, current_user, { label_id: params[:label_id] })
+
+ list = create_list_service.execute(board)
+
+ if list.valid?
+ present list, with: Entities::List
+ else
+ render_validation_error!(list)
+ end
+ end
+
+ def move_list(list)
+ move_list_service =
+ ::Boards::Lists::MoveService.new(board_parent, current_user, { position: params[:position].to_i })
+
+ if move_list_service.execute(list)
+ present list, with: Entities::List
+ else
+ render_api_error!({ error: "List could not be moved!" }, 400)
+ end
+ end
+
+ def destroy_list(list)
+ destroy_conditionally!(list) do |list|
+ service = ::Boards::Lists::DestroyService.new(board_parent, current_user)
+ unless service.execute(list)
+ render_api_error!({ error: 'List could not be deleted!' }, 400)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 4ad4a1f7867..86ac10c39d0 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -791,6 +791,8 @@ module API
class Board < Grape::Entity
expose :id
+ expose :project, using: Entities::BasicProjectDetails
+
expose :lists, using: Entities::List do |board|
board.lists.destroyable
end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 9ba15893f55..c1f5ec2ab14 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -74,8 +74,15 @@ module API
page || not_found!('Wiki Page')
end
- def available_labels
- @available_labels ||= LabelsFinder.new(current_user, project_id: user_project.id).execute
+ def available_labels_for(label_parent)
+ search_params =
+ if label_parent.is_a?(Project)
+ { project_id: label_parent.id }
+ else
+ { group_id: label_parent.id, only_group_labels: true }
+ end
+
+ LabelsFinder.new(current_user, search_params).execute
end
def find_user(id)
@@ -141,7 +148,9 @@ module API
end
def find_project_label(id)
- label = available_labels.find_by_id(id) || available_labels.find_by_title(id)
+ labels = available_labels_for(user_project)
+ label = labels.find_by_id(id) || labels.find_by_title(id)
+
label || not_found!('Label')
end
diff --git a/lib/api/labels.rb b/lib/api/labels.rb
index e41a1720ac1..81eaf56e48e 100644
--- a/lib/api/labels.rb
+++ b/lib/api/labels.rb
@@ -15,7 +15,7 @@ module API
use :pagination
end
get ':id/labels' do
- present paginate(available_labels), with: Entities::Label, current_user: current_user, project: user_project
+ present paginate(available_labels_for(user_project)), with: Entities::Label, current_user: current_user, project: user_project
end
desc 'Create a new label' do
@@ -30,7 +30,7 @@ module API
post ':id/labels' do
authorize! :admin_label, user_project
- label = available_labels.find_by(title: params[:name])
+ label = available_labels_for(user_project).find_by(title: params[:name])
conflict!('Label already exists') if label
priority = params.delete(:priority)
diff --git a/lib/api/v3/labels.rb b/lib/api/v3/labels.rb
index bd5eb2175e8..4157462ec2a 100644
--- a/lib/api/v3/labels.rb
+++ b/lib/api/v3/labels.rb
@@ -11,7 +11,7 @@ module API
success ::API::Entities::Label
end
get ':id/labels' do
- present available_labels, with: ::API::Entities::Label, current_user: current_user, project: user_project
+ present available_labels_for(user_project), with: ::API::Entities::Label, current_user: current_user, project: user_project
end
desc 'Delete an existing label' do
diff --git a/spec/finders/labels_finder_spec.rb b/spec/finders/labels_finder_spec.rb
index d507af3fd3d..06031aee217 100644
--- a/spec/finders/labels_finder_spec.rb
+++ b/spec/finders/labels_finder_spec.rb
@@ -56,6 +56,16 @@ describe LabelsFinder do
expect(finder.execute).to eq [group_label_2, group_label_1, project_label_5]
end
+
+ context 'when only_group_labels is true' do
+ it 'returns only group labels' do
+ group_1.add_developer(user)
+
+ finder = described_class.new(user, group_id: group_1.id, only_group_labels: true)
+
+ expect(finder.execute).to eq [group_label_2, group_label_1]
+ end
+ end
end
context 'filtering by project_id' do
diff --git a/spec/fixtures/api/schemas/public_api/v4/board.json b/spec/fixtures/api/schemas/public_api/v4/board.json
new file mode 100644
index 00000000000..d667f1d631c
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/board.json
@@ -0,0 +1,86 @@
+{
+ "type": "object",
+ "required" : [
+ "id",
+ "project",
+ "lists"
+ ],
+ "properties" : {
+ "id": { "type": "integer" },
+ "project": {
+ "type": ["object", "null"],
+ "required": [
+ "id",
+ "avatar_url",
+ "description",
+ "default_branch",
+ "tag_list",
+ "ssh_url_to_repo",
+ "http_url_to_repo",
+ "web_url",
+ "name",
+ "name_with_namespace",
+ "path",
+ "path_with_namespace",
+ "star_count",
+ "forks_count",
+ "created_at",
+ "last_activity_at"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "avatar_url": { "type": ["string", "null"] },
+ "description": { "type": ["string", "null"] },
+ "default_branch": { "type": ["string", "null"] },
+ "tag_list": { "type": "array" },
+ "ssh_url_to_repo": { "type": "string" },
+ "http_url_to_repo": { "type": "string" },
+ "web_url": { "type": "string" },
+ "name": { "type": "string" },
+ "name_with_namespace": { "type": "string" },
+ "path": { "type": "string" },
+ "path_with_namespace": { "type": "string" },
+ "star_count": { "type": "integer" },
+ "forks_count": { "type": "integer" },
+ "created_at": { "type": "date" },
+ "last_activity_at": { "type": "date" }
+ },
+ "additionalProperties": false
+ },
+ "lists": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required" : [
+ "id",
+ "label",
+ "position"
+ ],
+ "properties" : {
+ "id": { "type": "integer" },
+ "label": {
+ "type": ["object", "null"],
+ "required": [
+ "id",
+ "color",
+ "description",
+ "name"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "color": {
+ "type": "string",
+ "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
+ },
+ "description": { "type": ["string", "null"] },
+ "name": { "type": "string" }
+ }
+ },
+ "position": { "type": ["integer", "null"] }
+ },
+ "additionalProperties": false
+ }
+ }
+ },
+ "additionalProperties": true
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/boards.json b/spec/fixtures/api/schemas/public_api/v4/boards.json
new file mode 100644
index 00000000000..117564ef77a
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/boards.json
@@ -0,0 +1,4 @@
+{
+ "type": "array",
+ "items": { "$ref": "board.json" }
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/user/basic.json b/spec/fixtures/api/schemas/public_api/v4/user/basic.json
index 9f69d31971c..bf330d8278c 100644
--- a/spec/fixtures/api/schemas/public_api/v4/user/basic.json
+++ b/spec/fixtures/api/schemas/public_api/v4/user/basic.json
@@ -1,5 +1,5 @@
{
- "type": "object",
+ "type": ["object", "null"],
"required": [
"id",
"state",
diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb
index f65af69dc7f..c6c10025f7f 100644
--- a/spec/requests/api/boards_spec.rb
+++ b/spec/requests/api/boards_spec.rb
@@ -6,18 +6,18 @@ describe API::Boards do
set(:non_member) { create(:user) }
set(:guest) { create(:user) }
set(:admin) { create(:user, :admin) }
- set(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
+ set(:board_parent) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
set(:dev_label) do
- create(:label, title: 'Development', color: '#FFAABB', project: project)
+ create(:label, title: 'Development', color: '#FFAABB', project: board_parent)
end
set(:test_label) do
- create(:label, title: 'Testing', color: '#FFAACC', project: project)
+ create(:label, title: 'Testing', color: '#FFAACC', project: board_parent)
end
set(:ux_label) do
- create(:label, title: 'UX', color: '#FF0000', project: project)
+ create(:label, title: 'UX', color: '#FF0000', project: board_parent)
end
set(:dev_list) do
@@ -28,180 +28,25 @@ describe API::Boards do
create(:list, label: test_label, position: 2)
end
- set(:board) do
- create(:board, project: project, lists: [dev_list, test_list])
- end
-
- before do
- project.add_reporter(user)
- project.add_guest(guest)
- end
+ set(:milestone) { create(:milestone, project: board_parent) }
+ set(:board_label) { create(:label, project: board_parent) }
+ set(:board) { create(:board, project: board_parent, lists: [dev_list, test_list]) }
- describe "GET /projects/:id/boards" do
- let(:base_url) { "/projects/#{project.id}/boards" }
+ it_behaves_like 'group and project boards', "/projects/:id/boards"
- context "when unauthenticated" do
- it "returns authentication error" do
- get api(base_url)
-
- expect(response).to have_gitlab_http_status(401)
- end
- end
-
- context "when authenticated" do
- it "returns the project issue board" do
- get api(base_url, user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
- expect(json_response.first['id']).to eq(board.id)
- expect(json_response.first['lists']).to be_an Array
- expect(json_response.first['lists'].length).to eq(2)
- expect(json_response.first['lists'].last).to have_key('position')
- end
- end
- end
-
- describe "GET /projects/:id/boards/:board_id/lists" do
- let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
-
- it 'returns issue board lists' do
- get api(base_url, user)
-
- expect(response).to have_gitlab_http_status(200)
- 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("/projects/#{project.id}/boards/22343/lists", user)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- describe "GET /projects/:id/boards/:board_id/lists/:list_id" do
- let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
-
- it 'returns a list' do
- get api("#{base_url}/#{dev_list.id}", user)
-
- expect(response).to have_gitlab_http_status(200)
- 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("#{base_url}/5324", user)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- describe "POST /projects/:id/board/lists" do
- let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
+ describe "POST /projects/:id/boards/lists" do
+ let(:url) { "/projects/#{board_parent.id}/boards/#{board.id}/lists" }
it 'creates a new issue board list for group labels' do
group = create(:group)
group_label = create(:group_label, group: group)
- project.update(group: group)
+ board_parent.update(group: group)
- post api(base_url, user), label_id: group_label.id
+ post api(url, user), label_id: group_label.id
expect(response).to have_gitlab_http_status(201)
expect(json_response['label']['name']).to eq(group_label.title)
expect(json_response['position']).to eq(3)
end
-
- it 'creates a new issue board list for project labels' do
- post api(base_url, user), label_id: ux_label.id
-
- expect(response).to have_gitlab_http_status(201)
- 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(base_url, user), label_id: 23423
-
- expect(response).to have_gitlab_http_status(400)
- end
-
- it 'returns 403 for project members with guest role' do
- put api("#{base_url}/#{test_list.id}", guest), position: 1
-
- expect(response).to have_gitlab_http_status(403)
- end
- end
-
- describe "PUT /projects/:id/boards/:board_id/lists/:list_id to update only position" do
- let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
-
- it "updates a list" do
- put api("#{base_url}/#{test_list.id}", user),
- position: 1
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['position']).to eq(1)
- end
-
- it "returns 404 error if list id not found" do
- put api("#{base_url}/44444", user),
- position: 1
-
- expect(response).to have_gitlab_http_status(404)
- end
-
- it "returns 403 for project members with guest role" do
- put api("#{base_url}/#{test_list.id}", guest),
- position: 1
-
- expect(response).to have_gitlab_http_status(403)
- end
- end
-
- describe "DELETE /projects/:id/board/lists/:list_id" do
- let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
-
- it "rejects a non member from deleting a list" do
- delete api("#{base_url}/#{dev_list.id}", non_member)
-
- expect(response).to have_gitlab_http_status(403)
- end
-
- it "rejects a user with guest role from deleting a list" do
- delete api("#{base_url}/#{dev_list.id}", guest)
-
- expect(response).to have_gitlab_http_status(403)
- end
-
- it "returns 404 error if list id not found" do
- delete api("#{base_url}/44444", user)
-
- expect(response).to have_gitlab_http_status(404)
- end
-
- context "when the user is project owner" do
- set(:owner) { create(:user) }
-
- before do
- project.update(namespace: owner.namespace)
- end
-
- it "deletes the list if an admin requests it" do
- delete api("#{base_url}/#{dev_list.id}", owner)
-
- expect(response).to have_gitlab_http_status(204)
- end
-
- it_behaves_like '412 response' do
- let(:request) { api("#{base_url}/#{dev_list.id}", owner) }
- end
- end
end
end
diff --git a/spec/support/api/boards_shared_examples.rb b/spec/support/api/boards_shared_examples.rb
new file mode 100644
index 00000000000..943c1f6ffd7
--- /dev/null
+++ b/spec/support/api/boards_shared_examples.rb
@@ -0,0 +1,180 @@
+shared_examples_for '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
+
+ 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(401)
+ end
+ end
+
+ context "when authenticated" do
+ it "returns the issue boards" do
+ get api(root_url, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ 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(200)
+ 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(404)
+ 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(200)
+ 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(404)
+ 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), label_id: ux_label.id
+
+ expect(response).to have_gitlab_http_status(201)
+ 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), label_id: 23423
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+
+ it 'returns 403 for members with guest role' do
+ put api("#{url}/#{test_list.id}", guest), position: 1
+
+ expect(response).to have_gitlab_http_status(403)
+ 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),
+ position: 1
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['position']).to eq(1)
+ end
+
+ it "returns 404 error if list id not found" do
+ put api("#{url}/44444", user),
+ position: 1
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it "returns 403 for members with guest role" do
+ put api("#{url}/#{test_list.id}", guest),
+ position: 1
+
+ expect(response).to have_gitlab_http_status(403)
+ 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(403)
+ 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(403)
+ end
+
+ it "returns 404 error if list id not found" do
+ delete api("#{url}/44444", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ 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.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(204)
+ end
+
+ it_behaves_like '412 response' do
+ let(:request) { api("#{url}/#{dev_list.id}", owner) }
+ end
+ end
+ end
+end