diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index c26d40c5..354a95e9 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -91,7 +91,7 @@ class MergeRequest < ActiveRecord::Base def validate_branches if target_branch == source_branch - errors.add :base, "You can not use same branch for source and target branches" + errors.add :branch_conflict, "You can not use same branch for source and target branches" end end diff --git a/app/models/project.rb b/app/models/project.rb index 02f1df13..07ba7fc3 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -160,7 +160,7 @@ class Project < ActiveRecord::Base def check_limit unless creator.can_create_project? - errors[:base] << ("Your own projects limit is #{creator.projects_limit}! Please contact administrator to increase it") + errors[:limit_reached] << ("Your own projects limit is #{creator.projects_limit}! Please contact administrator to increase it") end rescue errors[:base] << ("Can't check your ability to create project") diff --git a/config/routes.rb b/config/routes.rb index 3550636c..b06fda8f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -8,6 +8,7 @@ Gitlab::Application.routes.draw do # API require 'api' + Gitlab::API.logger Rails.logger mount Gitlab::API => '/api' constraint = lambda { |request| request.env["warden"].authenticate? and request.env['warden'].user.admin? } diff --git a/doc/api/README.md b/doc/api/README.md index 0618db7e..f6c4e41b 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -1,6 +1,6 @@ # GitLab API -All API requests require authentication. You need to pass a `private_token` parameter by url or header. You can find or reset your private token in your profile. +All API requests require authentication. You need to pass a `private_token` parameter by url or header. If passed as header, the header name must be "PRIVATE-TOKEN" (capital and with dash instead of underscore). You can find or reset your private token in your profile. If no, or an invalid, `private_token` is provided then an error message will be returned with status code 401: @@ -18,8 +18,48 @@ Example of a valid API request: GET http://example.com/api/v3/projects?private_token=QVy1PB7sTxfy4pqfZM1U ``` +Example for a valid API request using curl and authentication via header: + +``` +curl --header "PRIVATE-TOKEN: QVy1PB7sTxfy4pqfZM1U" "http://example.com/api/v3/projects" +``` + + The API uses JSON to serialize data. You don't need to specify `.json` at the end of API URL. + + +## Status codes + +The API is designed to return different status codes according to context and action. In this way +if a request results in an error the caller is able to get insight into what went wrong, e.g. +status code `400 Bad Request` is returned if a required attribute is missing from the request. +The following list gives an overview of how the API functions generally behave. + +API request types: + +* `GET` requests access one or more resources and return the result as JSON +* `POST` requests return `201 Created` if the resource is successfully created and return the newly created resource as JSON +* `GET`, `PUT` and `DELETE` return `200 Ok` if the resource is accessed, modified or deleted successfully, the (modified) result is returned as JSON +* `DELETE` requests are designed to be idempotent, meaning a request a resource still returns `200 Ok` even it was deleted before or is not available. The reasoning behind it is the user is not really interested if the resource existed before or not. + + +The following list shows the possible return codes for API requests. + +Return values: + +* `200 Ok` - The `GET`, `PUT` or `DELETE` request was successful, the resource(s) itself is returned as JSON +* `201 Created` - The `POST` request was successful and the resource is returned as JSON +* `400 Bad Request` - A required attribute of the API request is missing, e.g. the title of an issue is not given +* `401 Unauthorized` - The user is not authenticated, a valid user token is necessary, see above +* `403 Forbidden` - The request is not allowed, e.g. the user is not allowed to delete a project +* `404 Not Found` - A resource could not be accessed, e.g. an ID for a resource could not be found +* `405 Method Not Allowed` - The request is not supported +* `409 Conflict` - A conflicting resource already exists, e.g. creating a project with a name that already exists +* `500 Server Error` - While handling the request something went wrong on the server side + + + #### Pagination When listing resources you can pass the following parameters: diff --git a/doc/api/groups.md b/doc/api/groups.md index 4cde66b1..e9702ea2 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -17,7 +17,8 @@ GET /groups ] ``` -## Details of group + +## Details of a group Get all details of a group. @@ -29,19 +30,19 @@ Parameters: + `id` (required) - The ID of a group + ## New group -Create a new project group. Available only for admin +Creates a new project group. Available only for admin. ``` POST /groups ``` Parameters: -+ `name` (required) - Email -+ `path` - Password -Will return created group with status `201 Created` on success, or `404 Not found` on fail. ++ `name` (required) - The name of the group ++ `path` (required) - The path of the group ## Transfer project to group diff --git a/doc/api/issues.md b/doc/api/issues.md index 0383b676..a8ae7401 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -1,6 +1,7 @@ ## List issues -Get all issues created by authenticed user. +Get all issues created by authenticed user. This function takes pagination parameters +`page` and `per_page` to restrict the list of issues. ``` GET /issues @@ -68,9 +69,11 @@ GET /issues ] ``` + ## List project issues -Get a list of project issues. +Get a list of project issues. This function accepts pagination parameters `page` and `per_page` +to return the list of project issues. ``` GET /projects/:id/issues @@ -80,9 +83,10 @@ Parameters: + `id` (required) - The ID of a project + ## Single issue -Get a project issue. +Gets a single project issue. ``` GET /projects/:id/issues/:issue_id @@ -133,9 +137,10 @@ Parameters: } ``` + ## New issue -Create a new project issue. +Creates a new project issue. ``` POST /projects/:id/issues @@ -150,11 +155,10 @@ Parameters: + `milestone_id` (optional) - The ID of a milestone to assign issue + `labels` (optional) - Comma-separated label names for an issue -Will return created issue with status `201 Created` on success, or `404 Not found` on fail. ## Edit issue -Update an existing project issue. +Updates an existing project issue. This function is also used to mark an issue as closed. ``` PUT /projects/:id/issues/:issue_id @@ -171,5 +175,19 @@ Parameters: + `labels` (optional) - Comma-separated label names for an issue + `closed` (optional) - The state of an issue (0 = false, 1 = true) -Will return updated issue with status `200 OK` on success, or `404 Not found` on fail. + +## Delete existing issue (**Deprecated**) + +The function is deprecated and returns a `405 Method Not Allowed` +error if called. An issue gets now closed and is done by calling `PUT /projects/:id/issues/:issue_id` with +parameter `closed` set to 1. + +``` +DELETE /projects/:id/issues/:issue_id +``` + +Parameters: + ++ `id` (required) - The project ID ++ `issue_id` (required) - The ID of the issue diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 525c55d1..111c5211 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -1,6 +1,7 @@ ## List merge requests -Get all MR for this project. +Get all merge requests for this project. This function takes pagination parameters +`page` and `per_page` to restrict the list of merge requests. ``` GET /projects/:id/merge_requests @@ -40,9 +41,10 @@ Parameters: ] ``` -## Show MR -Show information about MR. +## Get single MR + +Shows information about a single merge request. ``` GET /projects/:id/merge_request/:merge_request_id @@ -84,7 +86,7 @@ Parameters: ## Create MR -Create MR. +Creates a new merge request. ``` POST /projects/:id/merge_requests @@ -126,9 +128,10 @@ Parameters: } ``` + ## Update MR -Update MR. You can change branches, title, or even close the MR. +Updates an existing merge request. You can change branches, title, or even close the MR. ``` PUT /projects/:id/merge_request/:merge_request_id @@ -172,9 +175,11 @@ Parameters: } } ``` + + ## Post comment to MR -Post comment to MR +Adds a comment to a merge request. ``` POST /projects/:id/merge_request/:merge_request_id/comments @@ -183,10 +188,9 @@ POST /projects/:id/merge_request/:merge_request_id/comments Parameters: + `id` (required) - The ID of a project -+ `merge_request_id` (required) - ID of MR ++ `merge_request_id` (required) - ID of merge request + `note` (required) - Text of comment -Will return created note with status `201 Created` on success, or `404 Not found` on fail. ```json { diff --git a/doc/api/milestones.md b/doc/api/milestones.md index 73d29afc..92a29cee 100644 --- a/doc/api/milestones.md +++ b/doc/api/milestones.md @@ -1,6 +1,6 @@ ## List project milestones -Get a list of project milestones. +Returns a list of project milestones. ``` GET /projects/:id/milestones @@ -10,9 +10,10 @@ Parameters: + `id` (required) - The ID of a project -## Single milestone -Get a single project milestone. +## Get single milestone + +Gets a single project milestone. ``` GET /projects/:id/milestones/:milestone_id @@ -23,9 +24,10 @@ Parameters: + `id` (required) - The ID of a project + `milestone_id` (required) - The ID of a project milestone -## New milestone -Create a new project milestone. +## Create new milestone + +Creates a new project milestone. ``` POST /projects/:id/milestones @@ -38,9 +40,10 @@ Parameters: + `description` (optional) - The description of the milestone + `due_date` (optional) - The due date of the milestone + ## Edit milestone -Update an existing project milestone. +Updates an existing project milestone. ``` PUT /projects/:id/milestones/:milestone_id @@ -54,3 +57,4 @@ Parameters: + `description` (optional) - The description of a milestone + `due_date` (optional) - The due date of the milestone + `closed` (optional) - The status of the milestone + diff --git a/doc/api/notes.md b/doc/api/notes.md index a4ba2826..4b57f636 100644 --- a/doc/api/notes.md +++ b/doc/api/notes.md @@ -1,4 +1,4 @@ -## List notes +## Wall ### List project wall notes @@ -30,50 +30,10 @@ Parameters: + `id` (required) - The ID of a project -### List merge request notes -Get a list of merge request notes. +### Get single wall note -``` -GET /projects/:id/merge_requests/:merge_request_id/notes -``` - -Parameters: - -+ `id` (required) - The ID of a project -+ `merge_request_id` (required) - The ID of an merge request - -### List issue notes - -Get a list of issue notes. - -``` -GET /projects/:id/issues/:issue_id/notes -``` - -Parameters: - -+ `id` (required) - The ID of a project -+ `issue_id` (required) - The ID of an issue - -### List snippet notes - -Get a list of snippet notes. - -``` -GET /projects/:id/snippets/:snippet_id/notes -``` - -Parameters: - -+ `id` (required) - The ID of a project -+ `snippet_id` (required) - The ID of a snippet - -## Single note - -### Single wall note - -Get a wall note. +Returns a single wall note. ``` GET /projects/:id/notes/:note_id @@ -84,39 +44,10 @@ Parameters: + `id` (required) - The ID of a project + `note_id` (required) - The ID of a wall note -### Single issue note -Get an issue note. +### Create new wall note -``` -GET /projects/:id/issues/:issue_id/:notes/:note_id -``` - -Parameters: - -+ `id` (required) - The ID of a project -+ `issue_id` (required) - The ID of a project issue -+ `note_id` (required) - The ID of an issue note - -### Single snippet note - -Get a snippet note. - -``` -GET /projects/:id/issues/:snippet_id/:notes/:note_id -``` - -Parameters: - -+ `id` (required) - The ID of a project -+ `snippet_id` (required) - The ID of a project snippet -+ `note_id` (required) - The ID of an snippet note - -## New note - -### New wall note - -Create a new wall note. +Creates a new wall note. ``` POST /projects/:id/notes @@ -127,12 +58,41 @@ Parameters: + `id` (required) - The ID of a project + `body` (required) - The content of a note -Will return created note with status `201 Created` on success, or `404 Not found` on fail. + +## Issues + +### List project issue notes + +Gets a list of all notes for a single issue. + +``` +GET /projects/:id/issues/:issue_id/notes +``` + +Parameters: + ++ `id` (required) - The ID of a project ++ `issue_id` (required) - The ID of an issue -### New issue note +### Get single issue note -Create a new issue note. +Returns a single note for a specific project issue + +``` +GET /projects/:id/issues/:issue_id/notes/:note_id +``` + +Parameters: + ++ `id` (required) - The ID of a project ++ `issue_id` (required) - The ID of a project issue ++ `note_id` (required) - The ID of an issue note + + +### Create new issue note + +Creates a new note to a single project issue. ``` POST /projects/:id/issues/:issue_id/notes @@ -144,11 +104,41 @@ Parameters: + `issue_id` (required) - The ID of an issue + `body` (required) - The content of a note -Will return created note with status `201 Created` on success, or `404 Not found` on fail. -### New snippet note +## Snippets -Create a new snippet note. +### List all snippet notes + +Gets a list of all notes for a single snippet. Snippet notes are comments users can post to a snippet. + +``` +GET /projects/:id/snippets/:snippet_id/notes +``` + +Parameters: + ++ `id` (required) - The ID of a project ++ `snippet_id` (required) - The ID of a project snippet + + +### Get single snippet note + +Returns a single note for a given snippet. + +``` +GET /projects/:id/snippets/:snippet_id/notes/:note_id +``` + +Parameters: + ++ `id` (required) - The ID of a project ++ `snippet_id` (required) - The ID of a project snippet ++ `note_id` (required) - The ID of an snippet note + + +### Create new snippet note + +Creates a new note for a single snippet. Snippet notes are comments users can post to a snippet. ``` POST /projects/:id/snippets/:snippet_id/notes @@ -160,4 +150,49 @@ Parameters: + `snippet_id` (required) - The ID of an snippet + `body` (required) - The content of a note -Will return created note with status `201 Created` on success, or `404 Not found` on fail. + +## Merge Requests + +### List all merge request notes + +Gets a list of all notes for a single merge request. + +``` +GET /projects/:id/merge_requests/:merge_request_id/notes +``` + +Parameters: + ++ `id` (required) - The ID of a project ++ `merge_request_id` (required) - The ID of a project merge request + + +### Get single merge request note + +Returns a single note for a given merge request. + +``` +GET /projects/:id/merge_requests/:merge_request_id/notes/:note_id +``` + +Parameters: + ++ `id` (required) - The ID of a project ++ `merge_request_id` (required) - The ID of a project merge request ++ `note_id` (required) - The ID of a merge request note + + +### Create new merge request note + +Creates a new note for a single merge request. + +``` +POST /projects/:id/merge_requests/:merge_request_id/notes +``` + +Parameters: + ++ `id` (required) - The ID of a project ++ `merge_request_id` (required) - The ID of a merge request ++ `body` (required) - The content of a note + diff --git a/doc/api/projects.md b/doc/api/projects.md index 4cbefbb6..d6a9a885 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -1,4 +1,6 @@ -## List projects +## Projects + +### List projects Get a list of projects owned by the authenticated user. @@ -55,9 +57,11 @@ GET /projects ] ``` -## Single project -Get a specific project, identified by project ID, which is owned by the authentication user. +### Get single project + +Get a specific project, identified by project ID or NAME, which is owned by the authentication user. +Currently namespaced projects cannot retrieved by name. ``` GET /projects/:id @@ -65,7 +69,7 @@ GET /projects/:id Parameters: -+ `id` (required) - The ID of a project ++ `id` (required) - The ID or NAME of a project ```json { @@ -92,9 +96,10 @@ Parameters: } ``` -## Create project -Create new project owned by user +### Create project + +Creates new project owned by user. ``` POST /projects @@ -110,12 +115,21 @@ Parameters: + `merge_requests_enabled` (optional) - enabled by default + `wiki_enabled` (optional) - enabled by default -Will return created project with status `201 Created` on success, or `404 Not -found` on fail. +**Project access levels** -## Create project for user +The project access levels are defined in the `user_project.rb` class. Currently, these levels are recoginized: -Create new project owned by user. Available only for admin +``` + GUEST = 10 + REPORTER = 20 + DEVELOPER = 30 + MASTER = 40 +``` + + +### Create project for user + +Creates a new project owned by user. Available only for admins. ``` POST /projects/user/:user_id @@ -132,10 +146,11 @@ Parameters: + `merge_requests_enabled` (optional) - enabled by default + `wiki_enabled` (optional) - enabled by default -Will return created project with status `201 Created` on success, or `404 Not -found` on fail. -## List project team members + +## Team members + +### List project team members Get a list of project team members. @@ -145,12 +160,13 @@ GET /projects/:id/members Parameters: -+ `id` (required) - The ID of a project -+ `query` - Query string ++ `id` (required) - The ID or NAME of a project ++ `query` (optional) - Query string to search for members -## Get project team member -Get a project team member. +### Get project team member + +Gets a project team member. ``` GET /projects/:id/members/:user_id @@ -158,12 +174,11 @@ GET /projects/:id/members/:user_id Parameters: -+ `id` (required) - The ID of a project ++ `id` (required) - The ID or NAME of a project + `user_id` (required) - The ID of a user ```json { - "id": 1, "username": "john_smith", "email": "john@example.com", @@ -174,9 +189,12 @@ Parameters: } ``` -## Add project team member -Add a user to a project team. +### Add project team member + +Adds a user to a project team. This is an idempotent method and can be called multiple times +with the same parameters. Adding team membership to a user that is already a member does not +affect the existing membership. ``` POST /projects/:id/members @@ -184,15 +202,14 @@ POST /projects/:id/members Parameters: -+ `id` (required) - The ID of a project ++ `id` (required) - The ID or NAME of a project + `user_id` (required) - The ID of a user to add + `access_level` (required) - Project access level -Will return status `201 Created` on success, or `404 Not found` on fail. -## Edit project team member +### Edit project team member -Update project team member to specified access level. +Updates project team member to a specified access level. ``` PUT /projects/:id/members/:user_id @@ -200,13 +217,12 @@ PUT /projects/:id/members/:user_id Parameters: -+ `id` (required) - The ID of a project ++ `id` (required) - The ID or NAME of a project + `user_id` (required) - The ID of a team member + `access_level` (required) - Project access level -Will return status `200 OK` on success, or `404 Not found` on fail. -## Remove project team member +### Remove project team member Removes user from project team. @@ -216,14 +232,20 @@ DELETE /projects/:id/members/:user_id Parameters: -+ `id` (required) - The ID of a project ++ `id` (required) - The ID or NAME of a project + `user_id` (required) - The ID of a team member -Status code `200` will be returned on success. +This method is idempotent and can be called multiple times with the same parameters. +Revoking team membership for a user who is not currently a team member is considered success. +Please note that the returned JSON currently differs slightly. Thus you should not +rely on the returned JSON structure. -## List project hooks -Get list for project hooks +## Hooks + +### List project hooks + +Get list of project hooks. ``` GET /projects/:id/hooks @@ -231,13 +253,12 @@ GET /projects/:id/hooks Parameters: -+ `id` (required) - The ID of a project ++ `id` (required) - The ID or NAME of a project -Will return hooks with status `200 OK` on success, or `404 Not found` on fail. -## Get project hook +### Get project hook -Get hook for project +Get a specific hook for project. ``` GET /projects/:id/hooks/:hook_id @@ -245,14 +266,21 @@ GET /projects/:id/hooks/:hook_id Parameters: -+ `id` (required) - The ID of a project ++ `id` (required) - The ID or NAME of a project + `hook_id` (required) - The ID of a project hook -Will return hook with status `200 OK` on success, or `404 Not found` on fail. +```json +{ + "id": 1, + "url": "http://example.com/hook", + "created_at": "2012-10-12T17:04:47Z" +} +``` -## Add project hook -Add hook to project +### Add project hook + +Adds a hook to project. ``` POST /projects/:id/hooks @@ -260,14 +288,13 @@ POST /projects/:id/hooks Parameters: -+ `id` (required) - The ID of a project ++ `id` (required) - The ID or NAME of a project + `url` (required) - The hook URL -Will return status `201 Created` on success, or `404 Not found` on fail. -## Edit project hook +### Edit project hook -Edit hook for project +Edits a hook for project. ``` PUT /projects/:id/hooks/:hook_id @@ -275,30 +302,125 @@ PUT /projects/:id/hooks/:hook_id Parameters: -+ `id` (required) - The ID of a project ++ `id` (required) - The ID or NAME of a project + `hook_id` (required) - The ID of a project hook + `url` (required) - The hook URL -Will return status `201 Created` on success, or `404 Not found` on fail. +### Delete project hook -## Delete project hook - -Delete hook from project +Removes a hook from project. This is an idempotent method and can be called multiple times. +Either the hook is available or not. ``` -DELETE /projects/:id/hooks/:hook_id +DELETE /projects/:id/hooks/ ``` Parameters: -+ `id` (required) - The ID of a project ++ `id` (required) - The ID or NAME of a project + `hook_id` (required) - The ID of hook to delete -Will return status `200 OK` on success, or `404 Not found` on fail. +Note the JSON response differs if the hook is available or not. If the project hook +is available before it is returned in the JSON response or an empty response is returned. -## List deploy keys +## Branches + +### List branches + +Lists all branches of a project. + +``` +GET /projects/:id/repository/branches +``` + +Parameters: + ++ `id` (required) - The ID of the project + + +### List single branch + +Lists a specific branch of a project. + +``` +GET /projects/:id/repository/branches/:branch +``` + +Parameters: + ++ `id` (required) - The ID of the project. ++ `branch` (required) - The name of the branch. + + +### Protect single branch + +Protects a single branch of a project. + +``` +PUT /projects/:id/repository/branches/:branch/protect +``` + +Parameters: + ++ `id` (required) - The ID of the project. ++ `branch` (required) - The name of the branch. + + +### Unprotect single branch + +Unprotects a single branch of a project. + +``` +PUT /projects/:id/repository/branches/:branch/unprotect +``` + +Parameters: + ++ `id` (required) - The ID of the project. ++ `branch` (required) - The name of the branch. + + +### List tags + +Lists all tags of a project. + +``` +GET /projects/:id/repository/tags +``` + +Parameters: + ++ `id` (required) - The ID of the project + + +### List commits + +Lists all commits with pagination. If the optional `ref_name` name is not given the commits of +the default branch (usually master) are returned. + +``` +GET /projects/:id/repository/commits +``` + +Parameters: + ++ `id` (required) - The Id of the project ++ `ref_name` (optional) - The name of a repository branch or tag ++ `page` (optional) - The page of commits to return (`0` default) ++ `per_page` (optional) - The number of commits per page (`20` default) + +Returns values: + ++ `200 Ok` on success and a list with commits ++ `404 Not Found` if project with id or the branch with `ref_name` not found + + + +## Deploy Keys + +### List deploy keys Get a list of a project's deploy keys. @@ -306,6 +428,10 @@ Get a list of a project's deploy keys. GET /projects/:id/keys ``` +Parameters: + ++ `id` (required) - The ID of the project + ```json [ { @@ -325,7 +451,8 @@ GET /projects/:id/keys ] ``` -## Single deploy key + +### Single deploy key Get a single key. @@ -335,7 +462,8 @@ GET /projects/:id/keys/:key_id Parameters: -+ `id` (required) - The ID of an deploy key ++ `id` (required) - The ID of the project ++ `key_id` (required) - The ID of the deploy key ```json { @@ -346,9 +474,11 @@ Parameters: soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=" } ``` -## Add deploy key -Create new deploy key for a project + +### Add deploy key + +Creates a new deploy key for a project. ``` POST /projects/:id/keys @@ -356,13 +486,12 @@ POST /projects/:id/keys Parameters: -+ `title` (required) - new deploy key's title -+ `key` (required) - new deploy key ++ `id` (required) - The ID of the project ++ `title` (required) - New deploy key's title ++ `key` (required) - New deploy key -Will return created key with status `201 Created` on success, or `404 Not -found` on fail. -## Delete deploy key +### Delete deploy key Delete a deploy key from a project @@ -372,6 +501,6 @@ DELETE /projects/:id/keys/:key_id Parameters: -+ `id` (required) - Deploy key ID ++ `id` (required) - The ID of the project ++ `key_id` (required) - The ID of the deploy key -Will return `200 OK` on success, or `404 Not Found` on fail. diff --git a/doc/api/repositories.md b/doc/api/repositories.md index fd0ef1f5..90fda387 100644 --- a/doc/api/repositories.md +++ b/doc/api/repositories.md @@ -1,4 +1,4 @@ -## Project repository branches +## List repository branches Get a list of repository branches from a project, sorted by name alphabetically. @@ -39,7 +39,8 @@ Parameters: ] ``` -## Project repository branch + +## Get single repository branch Get a single project repository branch. @@ -79,12 +80,11 @@ Parameters: } ``` -Will return status code `200` on success or `404 Not found` if the branch is not available. +## Protect repository branch -## Protect a project repository branch - -Protect a single project repository branch. +Protects a single project repository branch. This is an idempotent function, protecting an already +protected repository branch still returns a `200 Ok` status code. ``` PUT /projects/:id/repository/branches/:branch/protect @@ -122,9 +122,11 @@ Parameters: } ``` -## Unprotect a project repository branch -Unprotect a single project repository branch. +## Unprotect repository branch + +Unprotects a single project repository branch. This is an idempotent function, unprotecting an already +unprotected repository branch still returns a `200 Ok` status code. ``` PUT /projects/:id/repository/branches/:branch/unprotect @@ -162,7 +164,8 @@ Parameters: } ``` -## Project repository tags + +## List project repository tags Get a list of repository tags from a project, sorted by name in reverse alphabetical order. @@ -201,7 +204,8 @@ Parameters: ] ``` -## Project repository commits + +## List repository commits Get a list of repository commits in a project. @@ -212,7 +216,7 @@ GET /projects/:id/repository/commits Parameters: + `id` (required) - The ID of a project -+ `ref_name` (optional) - The name of a repository branch or tag ++ `ref_name` (optional) - The name of a repository branch or tag or if not given the default branch ```json [ @@ -235,6 +239,7 @@ Parameters: ] ``` + ## Raw blob content Get the raw file contents for a file. @@ -248,5 +253,3 @@ Parameters: + `id` (required) - The ID of a project + `sha` (required) - The commit or branch name + `filepath` (required) - The path the file - -Will return the raw file contents. diff --git a/doc/api/snippets.md b/doc/api/snippets.md index ceb8a63d..04ea367d 100644 --- a/doc/api/snippets.md +++ b/doc/api/snippets.md @@ -10,9 +10,10 @@ Parameters: + `id` (required) - The ID of a project + ## Single snippet -Get a project snippet. +Get a single project snippet. ``` GET /projects/:id/snippets/:snippet_id @@ -42,22 +43,10 @@ Parameters: } ``` -## Snippet content -Get a raw project snippet. +## Create new snippet -``` -GET /projects/:id/snippets/:snippet_id/raw -``` - -Parameters: - -+ `id` (required) - The ID of a project -+ `snippet_id` (required) - The ID of a project's snippet - -## New snippet - -Create a new project snippet. +Creates a new project snippet. The user must have permission to create new snippets. ``` POST /projects/:id/snippets @@ -71,11 +60,10 @@ Parameters: + `lifetime` (optional) - The expiration date of a snippet + `code` (required) - The content of a snippet -Will return created snippet with status `201 Created` on success, or `404 Not found` on fail. -## Edit snippet +## Update snippet -Update an existing project snippet. +Updates an existing project snippet. The user must have permission to change an existing snippet. ``` PUT /projects/:id/snippets/:snippet_id @@ -90,11 +78,11 @@ Parameters: + `lifetime` (optional) - The expiration date of a snippet + `code` (optional) - The content of a snippet -Will return updated snippet with status `200 OK` on success, or `404 Not found` on fail. ## Delete snippet -Delete existing project snippet. +Deletes an existing project snippet. This is an idempotent function and deleting a non-existent +snippet still returns a `200 Ok` status code. ``` DELETE /projects/:id/snippets/:snippet_id @@ -105,5 +93,16 @@ Parameters: + `id` (required) - The ID of a project + `snippet_id` (required) - The ID of a project's snippet -Status code `200` will be returned on success. +## Snippet content + +Returns the raw project snippet as plain text. + +``` +GET /projects/:id/snippets/:snippet_id/raw +``` + +Parameters: + ++ `id` (required) - The ID of a project ++ `snippet_id` (required) - The ID of a project's snippet diff --git a/doc/api/users.md b/doc/api/users.md index b75e84c6..dc31c10e 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -43,6 +43,7 @@ GET /users ] ``` + ## Single user Get a single user. @@ -74,37 +75,40 @@ Parameters: } ``` + ## User creation -Create user. Available only for admin + +Creates a new user. Note only administrators can create new users. ``` POST /users ``` Parameters: -+ `email` (required) - Email -+ `password` (required) - Password -+ `username` (required) - Username -+ `name` (required) - Name -+ `skype` - Skype ID -+ `linkedin` - Linkedin -+ `twitter` - Twitter account -+ `projects_limit` - Number of projects user can create -+ `extern_uid` - External UID -+ `provider` - External provider name -+ `bio` - User's bio -Will return created user with status `201 Created` on success, or `404 Not -found` on fail. ++ `email` (required) - Email ++ `password` (required) - Password ++ `username` (required) - Username ++ `name` (required) - Name ++ `skype` (optional) - Skype ID ++ `linkedin` (optional) - Linkedin ++ `twitter` (optional) - Twitter account ++ `projects_limit` (optional) - Number of projects user can create ++ `extern_uid` (optional) - External UID ++ `provider` (optional) - External provider name ++ `bio` (optional) - User's bio + ## User modification -Modify user. Available only for admin + +Modifies an existing user. Only administrators can change attributes of a user. ``` PUT /users/:id ``` Parameters: + + `email` - Email + `username` - Username + `name` - Name @@ -117,23 +121,28 @@ Parameters: + `provider` - External provider name + `bio` - User's bio +Note, at the moment this method does only return a 404 error, even in cases where a 409 (Conflict) would +be more appropriate, e.g. when renaming the email address to some exsisting one. -Will return created user with status `200 OK` on success, or `404 Not -found` on fail. ## User deletion -Delete user. Available only for admin + +Deletes a user. Available only for administrators. This is an idempotent function, calling this function +for a non-existent user id still returns a status code `200 Ok`. The JSON response differs if the user +was actually deleted or not. In the former the user is returned and in the latter not. ``` DELETE /users/:id ``` -Will return deleted user with status `200 OK` on success, or `404 Not -found` on fail. +Parameters: + ++ `id` (required) - The ID of the user + ## Current user -Get currently authenticated user. +Gets currently authenticated user. ``` GET /user @@ -156,6 +165,7 @@ GET /user } ``` + ## List SSH keys Get a list of currently authenticated user's SSH keys. @@ -183,6 +193,11 @@ GET /user/keys ] ``` +Parameters: + ++ **none** + + ## Single SSH key Get a single key. @@ -204,9 +219,11 @@ Parameters: soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=" } ``` + + ## Add SSH key -Create new key owned by currently authenticated user +Creates a new key owned by the currently authenticated user. ``` POST /user/keys @@ -217,8 +234,6 @@ Parameters: + `title` (required) - new SSH Key's title + `key` (required) - new SSH key -Will return created key with status `201 Created` on success, or `404 Not -found` on fail. ## Add SSH key for user @@ -239,7 +254,8 @@ found` on fail. ## Delete SSH key -Delete key owned by currently authenticated user +Deletes key owned by currently authenticated user. This is an idempotent function and calling it on a key that is already +deleted or not available results in `200 Ok`. ``` DELETE /user/keys/:id @@ -249,4 +265,3 @@ Parameters: + `id` (required) - SSH key ID -Will return `200 OK` on success, or `404 Not Found` on fail. diff --git a/lib/api.rb b/lib/api.rb index 2a9a0eb2..d241f9b7 100644 --- a/lib/api.rb +++ b/lib/api.rb @@ -8,6 +8,19 @@ module Gitlab rack_response({'message' => '404 Not found'}.to_json, 404) end + rescue_from :all do |exception| + # lifted from https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb#L60 + # why is this not wrapped in something reusable? + trace = exception.backtrace + + message = "\n#{exception.class} (#{exception.message}):\n" + message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code) + message << " " << trace.join("\n ") + + API.logger.add Logger::FATAL, message + rack_response({'message' => '500 Internal Server Error'}, 500) + end + format :json helpers APIHelpers diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 464a2d15..52fa8eff 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -20,12 +20,14 @@ module Gitlab # Create group. Available only for admin # # Parameters: - # name (required) - Name - # path (required) - Path + # name (required) - The name of the group + # path (required) - The path of the group # Example Request: # POST /groups post do authenticated_as_admin! + required_attributes! [:name, :path] + attrs = attributes_for_keys [:name, :path] @group = Group.new(attrs) @group.owner = current_user diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 6bd8111c..f12fb5fd 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -41,6 +41,17 @@ module Gitlab abilities.allowed?(object, action, subject) end + # Checks the occurrences of required attributes, each attribute must be present in the params hash + # or a Bad Request error is invoked. + # + # Parameters: + # keys (required) - A hash consisting of keys that must be present + def required_attributes!(keys) + keys.each do |key| + bad_request!(key) unless params[key].present? + end + end + def attributes_for_keys(keys) attrs = {} keys.each do |key| @@ -55,6 +66,12 @@ module Gitlab render_api_error!('403 Forbidden', 403) end + def bad_request!(attribute) + message = ["400 (Bad request)"] + message << "\"" + attribute.to_s + "\" not given" + render_api_error!(message.join(' '), 400) + end + def not_found!(resource = nil) message = ["404"] message << resource if resource diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 70bbf47e..500a8551 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -48,6 +48,7 @@ module Gitlab # Example Request: # POST /projects/:id/issues post ":id/issues" do + required_attributes! [:title] attrs = attributes_for_keys [:title, :description, :assignee_id, :milestone_id] attrs[:label_list] = params[:labels] if params[:labels].present? @issue = user_project.issues.new attrs diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 7f763eb4..234a005a 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -4,6 +4,16 @@ module Gitlab before { authenticate! } resource :projects do + helpers do + def handle_merge_request_errors!(errors) + if errors[:project_access].any? + error!(errors[:project_access], 422) + elsif errors[:branch_conflict].any? + error!(errors[:branch_conflict], 422) + end + not_found! + end + end # List merge requests # @@ -51,6 +61,7 @@ module Gitlab # post ":id/merge_requests" do authorize! :write_merge_request, user_project + required_attributes! [:source_branch, :target_branch, :title] attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title] merge_request = user_project.merge_requests.new(attrs) @@ -60,7 +71,7 @@ module Gitlab merge_request.reload_code present merge_request, with: Entities::MergeRequest else - not_found! + handle_merge_request_errors! merge_request.errors end end @@ -88,7 +99,7 @@ module Gitlab merge_request.mark_as_unchecked present merge_request, with: Entities::MergeRequest else - not_found! + handle_merge_request_errors! merge_request.errors end end @@ -102,6 +113,8 @@ module Gitlab # POST /projects/:id/merge_request/:merge_request_id/comments # post ":id/merge_request/:merge_request_id/comments" do + required_attributes! [:note] + merge_request = user_project.merge_requests.find(params[:merge_request_id]) note = merge_request.notes.new(note: params[:note], project_id: user_project.id) note.author = current_user diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb index eaf0d37c..1adeefec 100644 --- a/lib/api/milestones.rb +++ b/lib/api/milestones.rb @@ -41,6 +41,7 @@ module Gitlab # POST /projects/:id/milestones post ":id/milestones" do authorize! :admin_milestone, user_project + required_attributes! [:title] attrs = attributes_for_keys [:title, :description, :due_date] @milestone = user_project.milestones.new attrs diff --git a/lib/api/notes.rb b/lib/api/notes.rb index 70344d6e..097cc7ea 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -37,12 +37,16 @@ module Gitlab # Example Request: # POST /projects/:id/notes post ":id/notes" do + required_attributes! [:body] + @note = user_project.notes.new(note: params[:body]) @note.author = current_user if @note.save present @note, with: Entities::Note else + # :note is exposed as :body, but :note is set on error + bad_request!(:note) if @note.errors[:note].any? not_found! end end @@ -89,6 +93,8 @@ module Gitlab # POST /projects/:id/issues/:noteable_id/notes # POST /projects/:id/snippets/:noteable_id/notes post ":id/#{noteables_str}/:#{noteable_id_str}/notes" do + required_attributes! [:body] + @noteable = user_project.send(:"#{noteables_str}").find(params[:"#{noteable_id_str}"]) @note = @noteable.notes.new(note: params[:body]) @note.author = current_user diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 00b70728..e82cfeca 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -4,6 +4,15 @@ module Gitlab before { authenticate! } resource :projects do + helpers do + def handle_project_member_errors(errors) + if errors[:project_access].any? + error!(errors[:project_access], 422) + end + not_found! + end + end + # Get a projects list for authenticated user # # Example Request: @@ -33,9 +42,11 @@ module Gitlab # wall_enabled (optional) - enabled by default # merge_requests_enabled (optional) - enabled by default # wiki_enabled (optional) - enabled by default + # namespace_id (optional) - defaults to user namespace # Example Request # POST /projects post do + required_attributes! [:name] attrs = attributes_for_keys [:name, :description, :default_branch, @@ -48,6 +59,9 @@ module Gitlab if @project.saved? present @project, with: Entities::Project else + if @project.errors[:limit_reached].present? + error!(@project.errors[:limit_reached], 403) + end not_found! end end @@ -122,16 +136,22 @@ module Gitlab # POST /projects/:id/members post ":id/members" do authorize! :admin_project, user_project - users_project = user_project.users_projects.new( - user_id: params[:user_id], - project_access: params[:access_level] - ) + required_attributes! [:user_id, :access_level] - if users_project.save - @member = users_project.user + # either the user is already a team member or a new one + team_member = user_project.team_member_by_id(params[:user_id]) + if team_member.nil? + team_member = user_project.users_projects.new( + user_id: params[:user_id], + project_access: params[:access_level] + ) + end + + if team_member.save + @member = team_member.user present @member, with: Entities::ProjectMember, project: user_project else - not_found! + handle_project_member_errors team_member.errors end end @@ -145,13 +165,16 @@ module Gitlab # PUT /projects/:id/members/:user_id put ":id/members/:user_id" do authorize! :admin_project, user_project - users_project = user_project.users_projects.find_by_user_id params[:user_id] + required_attributes! [:access_level] - if users_project.update_attributes(project_access: params[:access_level]) - @member = users_project.user + team_member = user_project.users_projects.find_by_user_id(params[:user_id]) + not_found!("User can not be found") if team_member.nil? + + if team_member.update_attributes(project_access: params[:access_level]) + @member = team_member.user present @member, with: Entities::ProjectMember, project: user_project else - not_found! + handle_project_member_errors team_member.errors end end @@ -164,8 +187,12 @@ module Gitlab # DELETE /projects/:id/members/:user_id delete ":id/members/:user_id" do authorize! :admin_project, user_project - users_project = user_project.users_projects.find_by_user_id params[:user_id] - users_project.destroy + team_member = user_project.users_projects.find_by_user_id(params[:user_id]) + unless team_member.nil? + team_member.destroy + else + {:message => "Access revoked", :id => params[:user_id].to_i} + end end # Get project hooks @@ -203,11 +230,16 @@ module Gitlab # POST /projects/:id/hooks post ":id/hooks" do authorize! :admin_project, user_project + required_attributes! [:url] + @hook = user_project.hooks.new({"url" => params[:url]}) if @hook.save present @hook, with: Entities::Hook else - error!({'message' => '404 Not found'}, 404) + if @hook.errors[:url].present? + error!("Invalid url given", 422) + end + not_found! end end @@ -222,27 +254,36 @@ module Gitlab put ":id/hooks/:hook_id" do @hook = user_project.hooks.find(params[:hook_id]) authorize! :admin_project, user_project + required_attributes! [:url] attrs = attributes_for_keys [:url] - if @hook.update_attributes attrs present @hook, with: Entities::Hook else + if @hook.errors[:url].present? + error!("Invalid url given", 422) + end not_found! end end - # Delete project hook + # Deletes project hook. This is an idempotent function. # # Parameters: # id (required) - The ID of a project # hook_id (required) - The ID of hook to delete # Example Request: # DELETE /projects/:id/hooks/:hook_id - delete ":id/hooks/:hook_id" do + delete ":id/hooks" do authorize! :admin_project, user_project - @hook = user_project.hooks.find(params[:hook_id]) - @hook.destroy + required_attributes! [:hook_id] + + begin + @hook = ProjectHook.find(params[:hook_id]) + @hook.destroy + rescue + # ProjectHook can raise Error if hook_id not found + end end # Get a project repository branches @@ -277,6 +318,7 @@ module Gitlab # PUT /projects/:id/repository/branches/:branch/protect put ":id/repository/branches/:branch/protect" do @branch = user_project.repo.heads.find { |item| item.name == params[:branch] } + not_found! unless @branch protected = user_project.protected_branches.find_by_name(@branch.name) unless protected @@ -295,6 +337,7 @@ module Gitlab # PUT /projects/:id/repository/branches/:branch/unprotect put ":id/repository/branches/:branch/unprotect" do @branch = user_project.repo.heads.find { |item| item.name == params[:branch] } + not_found! unless @branch protected = user_project.protected_branches.find_by_name(@branch.name) if protected @@ -318,7 +361,7 @@ module Gitlab # # Parameters: # id (required) - The ID of a project - # ref_name (optional) - The name of a repository branch or tag + # ref_name (optional) - The name of a repository branch or tag, if not given the default branch is used # Example Request: # GET /projects/:id/repository/commits get ":id/repository/commits" do @@ -366,6 +409,7 @@ module Gitlab # POST /projects/:id/snippets post ":id/snippets" do authorize! :write_snippet, user_project + required_attributes! [:title, :file_name, :code] attrs = attributes_for_keys [:title, :file_name] attrs[:expires_at] = params[:lifetime] if params[:lifetime].present? @@ -414,10 +458,12 @@ module Gitlab # Example Request: # DELETE /projects/:id/snippets/:snippet_id delete ":id/snippets/:snippet_id" do - @snippet = user_project.snippets.find(params[:snippet_id]) - authorize! :modify_snippet, @snippet - - @snippet.destroy + begin + @snippet = user_project.snippets.find(params[:snippet_id]) + authorize! :modify_snippet, user_project + @snippet.destroy + rescue + end end # Get a raw project snippet @@ -443,6 +489,7 @@ module Gitlab # GET /projects/:id/repository/commits/:sha/blob get ":id/repository/commits/:sha/blob" do authorize! :download_code, user_project + required_attributes! [:filepath] ref = params[:sha] diff --git a/lib/api/users.rb b/lib/api/users.rb index 7399d1a5..6cc3a7e5 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -41,6 +41,8 @@ module Gitlab # POST /users post do authenticated_as_admin! + required_attributes! [:email, :password, :name, :username] + attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :extern_uid, :provider, :bio] user = User.new attrs, as: :admin if user.save @@ -67,10 +69,12 @@ module Gitlab # PUT /users/:id put ":id" do authenticated_as_admin! - attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :extern_uid, :provider, :bio] - user = User.find_by_id(params[:id]) - if user && user.update_attributes(attrs) + attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :extern_uid, :provider, :bio] + user = User.find(params[:id]) + not_found!("User not found") unless user + + if user.update_attributes(attrs) present user, with: Entities::User else not_found! @@ -147,6 +151,8 @@ module Gitlab # Example Request: # POST /user/keys post "keys" do + required_attributes! [:title, :key] + attrs = attributes_for_keys [:title, :key] key = current_user.keys.new attrs if key.save @@ -156,15 +162,18 @@ module Gitlab end end - # Delete existed ssh key of currently authenticated user + # Delete existing ssh key of currently authenticated user # # Parameters: # id (required) - SSH Key ID # Example Request: # DELETE /user/keys/:id delete "keys/:id" do - key = current_user.keys.find params[:id] - key.delete + begin + key = current_user.keys.find params[:id] + key.delete + rescue + end end end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 44f4cd4a..545908b2 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -65,7 +65,7 @@ describe Project do it "should not allow new projects beyond user limits" do project.stub(:creator).and_return(double(can_create_project?: false, projects_limit: 1)) project.should_not be_valid - project.errors[:base].first.should match(/Your own projects limit is 1/) + project.errors[:limit_reached].first.should match(/Your own projects limit is 1/) end end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 63616eef..e97ceb2c 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -88,6 +88,16 @@ describe Gitlab::API do post api("/groups", admin), {:name => "Duplicate Test", :path => group2.path} response.status.should == 404 end + + it "should return 400 bad request error if name not given" do + post api("/groups", admin), { :path => group2.path } + response.status.should == 400 + end + + it "should return 400 bad request error if path not given" do + post api("/groups", admin), { :name => 'test' } + response.status.should == 400 + end end end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 630ac0f8..ecf0bdb7 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -41,6 +41,11 @@ describe Gitlab::API do response.status.should == 200 json_response['title'].should == issue.title end + + it "should return 404 if issue id not found" do + get api("/projects/#{project.id}/issues/54321", user) + response.status.should == 404 + end end describe "POST /projects/:id/issues" do @@ -52,6 +57,11 @@ describe Gitlab::API do json_response['description'].should be_nil json_response['labels'].should == ['label', 'label2'] end + + it "should return a 400 bad request if title not given" do + post api("/projects/#{project.id}/issues", user), labels: 'label, label2' + response.status.should == 400 + end end describe "PUT /projects/:id/issues/:issue_id to update only title" do @@ -62,6 +72,12 @@ describe Gitlab::API do json_response['title'].should == 'updated title' end + + it "should return 404 error if issue id not found" do + put api("/projects/#{project.id}/issues/44444", user), + title: 'updated title' + response.status.should == 404 + end end describe "PUT /projects/:id/issues/:issue_id to update state and label" do diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 1abd7a20..e7af056a 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -32,6 +32,11 @@ describe Gitlab::API do response.status.should == 200 json_response['title'].should == merge_request.title end + + it "should return a 404 error if merge_request_id not found" do + get api("/projects/#{project.id}/merge_request/999", user) + response.status.should == 404 + end end describe "POST /projects/:id/merge_requests" do @@ -41,6 +46,30 @@ describe Gitlab::API do response.status.should == 201 json_response['title'].should == 'Test merge_request' end + + it "should return 422 when source_branch equals target_branch" do + post api("/projects/#{project.id}/merge_requests", user), + title: "Test merge_request", source_branch: "master", target_branch: "master", author: user + response.status.should == 422 + end + + it "should return 400 when source_branch is missing" do + post api("/projects/#{project.id}/merge_requests", user), + title: "Test merge_request", target_branch: "master", author: user + response.status.should == 400 + end + + it "should return 400 when target_branch is missing" do + post api("/projects/#{project.id}/merge_requests", user), + title: "Test merge_request", source_branch: "stable", author: user + response.status.should == 400 + end + + it "should return 400 when title is missing" do + post api("/projects/#{project.id}/merge_requests", user), + target_branch: 'master', source_branch: 'stable' + response.status.should == 400 + end end describe "PUT /projects/:id/merge_request/:merge_request_id to close MR" do @@ -59,13 +88,24 @@ describe Gitlab::API do end end - describe "PUT /projects/:id/merge_request/:merge_request_id" do it "should return merge_request" do put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), title: "New title" response.status.should == 200 json_response['title'].should == 'New title' end + + it "should return 422 when source_branch and target_branch are renamed the same" do + put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), + source_branch: "master", target_branch: "master" + response.status.should == 422 + end + + it "should return merge_request with renamed target_branch" do + put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), target_branch: "test" + response.status.should == 200 + json_response['target_branch'].should == 'test' + end end describe "POST /projects/:id/merge_request/:merge_request_id/comments" do @@ -74,6 +114,16 @@ describe Gitlab::API do response.status.should == 201 json_response['note'].should == 'My comment' end + + it "should return 400 if note is missing" do + post api("/projects/#{project.id}/merge_request/#{merge_request.id}/comments", user) + response.status.should == 400 + end + + it "should return 404 if note is attached to non existent merge request" do + post api("/projects/#{project.id}/merge_request/111/comments", user), note: "My comment" + response.status.should == 404 + end end end diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb index d1b5e449..c379e8a5 100644 --- a/spec/requests/api/milestones_spec.rb +++ b/spec/requests/api/milestones_spec.rb @@ -16,6 +16,11 @@ describe Gitlab::API do json_response.should be_an Array json_response.first['title'].should == milestone.title end + + it "should return a 401 error if user not authenticated" do + get api("/projects/#{project.id}/milestones") + response.status.should == 401 + end end describe "GET /projects/:id/milestones/:milestone_id" do @@ -24,16 +29,38 @@ describe Gitlab::API do response.status.should == 200 json_response['title'].should == milestone.title end + + it "should return 401 error if user not authenticated" do + get api("/projects/#{project.id}/milestones/#{milestone.id}") + response.status.should == 401 + end + + it "should return a 404 error if milestone id not found" do + get api("/projects/#{project.id}/milestones/1234", user) + response.status.should == 404 + end end describe "POST /projects/:id/milestones" do it "should create a new project milestone" do - post api("/projects/#{project.id}/milestones", user), - title: 'new milestone' + post api("/projects/#{project.id}/milestones", user), title: 'new milestone' response.status.should == 201 json_response['title'].should == 'new milestone' json_response['description'].should be_nil end + + it "should create a new project milestone with description and due date" do + post api("/projects/#{project.id}/milestones", user), + title: 'new milestone', description: 'release', due_date: '2013-03-02' + response.status.should == 201 + json_response['description'].should == 'release' + json_response['due_date'].should == '2013-03-02' + end + + it "should return a 400 error if title is missing" do + post api("/projects/#{project.id}/milestones", user) + response.status.should == 400 + end end describe "PUT /projects/:id/milestones/:milestone_id" do @@ -43,6 +70,12 @@ describe Gitlab::API do response.status.should == 200 json_response['title'].should == 'updated title' end + + it "should return a 404 error if milestone id not found" do + put api("/projects/#{project.id}/milestones/1234", user), + title: 'updated title' + response.status.should == 404 + end end describe "PUT /projects/:id/milestones/:milestone_id to close milestone" do diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index ee99d85d..90164083 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -38,6 +38,11 @@ describe Gitlab::API do response.status.should == 200 json_response['body'].should == wall_note.note end + + it "should return a 404 error if note not found" do + get api("/projects/#{project.id}/notes/123", user) + response.status.should == 404 + end end describe "POST /projects/:id/notes" do @@ -46,6 +51,16 @@ describe Gitlab::API do response.status.should == 201 json_response['body'].should == 'hi!' end + + it "should return 401 unauthorized error" do + post api("/projects/#{project.id}/notes") + response.status.should == 401 + end + + it "should return a 400 bad request if body is missing" do + post api("/projects/#{project.id}/notes", user) + response.status.should == 400 + end end describe "GET /projects/:id/noteable/:noteable_id/notes" do @@ -56,6 +71,11 @@ describe Gitlab::API do json_response.should be_an Array json_response.first['body'].should == issue_note.note end + + it "should return a 404 error when issue id not found" do + get api("/projects/#{project.id}/issues/123/notes", user) + response.status.should == 404 + end end context "when noteable is a Snippet" do @@ -65,6 +85,11 @@ describe Gitlab::API do json_response.should be_an Array json_response.first['body'].should == snippet_note.note end + + it "should return a 404 error when snippet id not found" do + get api("/projects/#{project.id}/snippets/42/notes", user) + response.status.should == 404 + end end context "when noteable is a Merge Request" do @@ -74,6 +99,11 @@ describe Gitlab::API do json_response.should be_an Array json_response.first['body'].should == merge_request_note.note end + + it "should return a 404 error if merge request id not found" do + get api("/projects/#{project.id}/merge_requests/4444/notes", user) + response.status.should == 404 + end end end @@ -84,6 +114,11 @@ describe Gitlab::API do response.status.should == 200 json_response['body'].should == issue_note.note end + + it "should return a 404 error if issue note not found" do + get api("/projects/#{project.id}/issues/#{issue.id}/notes/123", user) + response.status.should == 404 + end end context "when noteable is a Snippet" do @@ -92,6 +127,11 @@ describe Gitlab::API do response.status.should == 200 json_response['body'].should == snippet_note.note end + + it "should return a 404 error if snippet note not found" do + get api("/projects/#{project.id}/snippets/#{snippet.id}/notes/123", user) + response.status.should == 404 + end end end @@ -103,6 +143,16 @@ describe Gitlab::API do json_response['body'].should == 'hi!' json_response['author']['email'].should == user.email end + + it "should return a 400 bad request error if body not given" do + post api("/projects/#{project.id}/issues/#{issue.id}/notes", user) + response.status.should == 400 + end + + it "should return a 401 unauthorized error if user not authenticated" do + post api("/projects/#{project.id}/issues/#{issue.id}/notes"), body: 'hi!' + response.status.should == 401 + end end context "when noteable is a Snippet" do @@ -112,6 +162,16 @@ describe Gitlab::API do json_response['body'].should == 'hi!' json_response['author']['email'].should == user.email end + + it "should return a 400 bad request error if body not given" do + post api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user) + response.status.should == 400 + end + + it "should return a 401 unauthorized error if user not authenticated" do + post api("/projects/#{project.id}/snippets/#{snippet.id}/notes"), body: 'hi!' + response.status.should == 401 + end end end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 72f93c46..cddb7264 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -7,8 +7,8 @@ describe Gitlab::API do let(:user2) { create(:user) } let(:user3) { create(:user) } let(:admin) { create(:admin) } - let!(:hook) { create(:project_hook, project: project, url: "http://example.com") } let!(:project) { create(:project, namespace: user.namespace ) } + let!(:hook) { create(:project_hook, project: project, url: "http://example.com") } let!(:snippet) { create(:snippet, author: user, project: project, title: 'example') } let!(:users_project) { create(:users_project, user: user, project: project, project_access: UsersProject::MASTER) } let!(:users_project2) { create(:users_project, user: user3, project: project, project_access: UsersProject::DEVELOPER) } @@ -58,6 +58,11 @@ describe Gitlab::API do expect { post api("/projects", user) }.to_not change {Project.count} end + it "should return a 400 error if name not given" do + post api("/projects", user) + response.status.should == 400 + end + it "should create last project before reaching project limit" do (1..user2.projects_limit-1).each { |p| post api("/projects", user2), name: "foo#{p}" } post api("/projects", user2), name: "foo" @@ -69,9 +74,17 @@ describe Gitlab::API do response.status.should == 201 end - it "should respond with 404 on failure" do + it "should respond with 400 if name is not given" do post api("/projects", user) - response.status.should == 404 + response.status.should == 400 + end + + it "should return a 403 error if project limit reached" do + (1..user.projects_limit).each do |p| + post api("/projects", user), name: "foo#{p}" + end + post api("/projects", user), name: 'bar' + response.status.should == 403 end it "should assign attributes to project" do @@ -152,6 +165,12 @@ describe Gitlab::API do response.status.should == 404 json_response['message'].should == '404 Not Found' end + + it "should return a 404 error if user is not a member" do + other_user = create(:user) + get api("/projects/#{project.id}", other_user) + response.status.should == 404 + end end describe "GET /projects/:id/repository/branches" do @@ -188,6 +207,17 @@ describe Gitlab::API do json_response['commit']['id'].should == '621491c677087aa243f165eab467bfdfbee00be1' json_response['protected'].should == true end + + it "should return a 404 error if branch not found" do + put api("/projects/#{project.id}/repository/branches/unknown/protect", user) + response.status.should == 404 + end + + it "should return success when protect branch again" do + put api("/projects/#{project.id}/repository/branches/new_design/protect", user) + put api("/projects/#{project.id}/repository/branches/new_design/protect", user) + response.status.should == 200 + end end describe "PUT /projects/:id/repository/branches/:branch/unprotect" do @@ -199,6 +229,17 @@ describe Gitlab::API do json_response['commit']['id'].should == '621491c677087aa243f165eab467bfdfbee00be1' json_response['protected'].should == false end + + it "should return success when unprotect branch" do + put api("/projects/#{project.id}/repository/branches/unknown/unprotect", user) + response.status.should == 404 + end + + it "should return success when unprotect branch again" do + put api("/projects/#{project.id}/repository/branches/new_design/unprotect", user) + put api("/projects/#{project.id}/repository/branches/new_design/unprotect", user) + response.status.should == 200 + end end describe "GET /projects/:id/members" do @@ -217,6 +258,11 @@ describe Gitlab::API do json_response.count.should == 1 json_response.first['email'].should == user.email end + + it "should return a 404 error if id not found" do + get api("/projects/9999/members", user) + response.status.should == 404 + end end describe "GET /projects/:id/members/:user_id" do @@ -226,6 +272,11 @@ describe Gitlab::API do json_response['email'].should == user.email json_response['access_level'].should == UsersProject::MASTER end + + it "should return a 404 error if user id not found" do + get api("/projects/#{project.id}/members/1234", user) + response.status.should == 404 + end end describe "POST /projects/:id/members" do @@ -239,6 +290,34 @@ describe Gitlab::API do json_response['email'].should == user2.email json_response['access_level'].should == UsersProject::DEVELOPER end + + it "should return a 201 status if user is already project member" do + post api("/projects/#{project.id}/members", user), user_id: user2.id, + access_level: UsersProject::DEVELOPER + expect { + post api("/projects/#{project.id}/members", user), user_id: user2.id, + access_level: UsersProject::DEVELOPER + }.not_to change { UsersProject.count }.by(1) + + response.status.should == 201 + json_response['email'].should == user2.email + json_response['access_level'].should == UsersProject::DEVELOPER + end + + it "should return a 400 error when user id is not given" do + post api("/projects/#{project.id}/members", user), access_level: UsersProject::MASTER + response.status.should == 400 + end + + it "should return a 400 error when access level is not given" do + post api("/projects/#{project.id}/members", user), user_id: user2.id + response.status.should == 400 + end + + it "should return a 422 error when access level is not known" do + post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: 1234 + response.status.should == 422 + end end describe "PUT /projects/:id/members/:user_id" do @@ -248,6 +327,21 @@ describe Gitlab::API do json_response['email'].should == user3.email json_response['access_level'].should == UsersProject::MASTER end + + it "should return a 404 error if user_id is not found" do + put api("/projects/#{project.id}/members/1234", user), access_level: UsersProject::MASTER + response.status.should == 404 + end + + it "should return a 400 error when access level is not given" do + put api("/projects/#{project.id}/members/#{user3.id}", user) + response.status.should == 400 + end + + it "should return a 422 error when access level is not known" do + put api("/projects/#{project.id}/members/#{user3.id}", user), access_level: 123 + response.status.should == 422 + end end describe "DELETE /projects/:id/members/:user_id" do @@ -256,6 +350,30 @@ describe Gitlab::API do delete api("/projects/#{project.id}/members/#{user3.id}", user) }.to change { UsersProject.count }.by(-1) end + + it "should return 200 if team member is not part of a project" do + delete api("/projects/#{project.id}/members/#{user3.id}", user) + expect { + delete api("/projects/#{project.id}/members/#{user3.id}", user) + }.to_not change { UsersProject.count }.by(1) + end + + it "should return 200 if team member already removed" do + delete api("/projects/#{project.id}/members/#{user3.id}", user) + delete api("/projects/#{project.id}/members/#{user3.id}", user) + response.status.should == 200 + end + end + + describe "DELETE /projects/:id/members/:user_id" do + it "should return 200 OK when the user was not member" do + expect { + delete api("/projects/#{project.id}/members/1000000", user) + }.to change { UsersProject.count }.by(0) + response.status.should == 200 + json_response['message'].should == "Access revoked" + json_response['id'].should == 1000000 + end end describe "GET /projects/:id/hooks" do @@ -298,6 +416,11 @@ describe Gitlab::API do response.status.should == 403 end end + + it "should return a 404 error if hook id is not available" do + get api("/projects/#{project.id}/hooks/1234", user) + response.status.should == 404 + end end describe "POST /projects/:id/hooks" do @@ -306,6 +429,17 @@ describe Gitlab::API do post api("/projects/#{project.id}/hooks", user), url: "http://example.com" }.to change {project.hooks.count}.by(1) + response.status.should == 201 + end + + it "should return a 400 error if url not given" do + post api("/projects/#{project.id}/hooks", user) + response.status.should == 400 + end + + it "should return a 422 error if url not valid" do + post api("/projects/#{project.id}/hooks", user), "url" => "ftp://example.com" + response.status.should == 422 end end @@ -316,13 +450,44 @@ describe Gitlab::API do response.status.should == 200 json_response['url'].should == 'http://example.org' end + + it "should return 404 error if hook id not found" do + put api("/projects/#{project.id}/hooks/1234", user), url: 'http://example.org' + response.status.should == 404 + end + + it "should return 400 error if url is not given" do + put api("/projects/#{project.id}/hooks/#{hook.id}", user) + response.status.should == 400 + end + + it "should return a 422 error if url is not valid" do + put api("/projects/#{project.id}/hooks/#{hook.id}", user), url: 'ftp://example.com' + response.status.should == 422 + end end - describe "DELETE /projects/:id/hooks/:hook_id" do + describe "DELETE /projects/:id/hooks" do it "should delete hook from project" do expect { - delete api("/projects/#{project.id}/hooks/#{hook.id}", user) + delete api("/projects/#{project.id}/hooks", user), hook_id: hook.id }.to change {project.hooks.count}.by(-1) + response.status.should == 200 + end + + it "should return success when deleting hook" do + delete api("/projects/#{project.id}/hooks", user), hook_id: hook.id + response.status.should == 200 + end + + it "should return success when deleting non existent hook" do + delete api("/projects/#{project.id}/hooks", user), hook_id: 42 + response.status.should == 200 + end + + it "should return a 400 error if hook id not given" do + delete api("/projects/#{project.id}/hooks", user) + response.status.should == 400 end end @@ -371,6 +536,11 @@ describe Gitlab::API do response.status.should == 200 json_response['title'].should == snippet.title end + + it "should return a 404 error if snippet id not found" do + get api("/projects/#{project.id}/snippets/1234", user) + response.status.should == 404 + end end describe "POST /projects/:id/snippets" do @@ -380,6 +550,24 @@ describe Gitlab::API do response.status.should == 201 json_response['title'].should == 'api test' end + + it "should return a 400 error if title is not given" do + post api("/projects/#{project.id}/snippets", user), + file_name: 'sample.rb', code: 'test' + response.status.should == 400 + end + + it "should return a 400 error if file_name not given" do + post api("/projects/#{project.id}/snippets", user), + title: 'api test', code: 'test' + response.status.should == 400 + end + + it "should return a 400 error if code not given" do + post api("/projects/#{project.id}/snippets", user), + title: 'api test', file_name: 'sample.rb' + response.status.should == 400 + end end describe "PUT /projects/:id/snippets/:shippet_id" do @@ -390,6 +578,13 @@ describe Gitlab::API do json_response['title'].should == 'example' snippet.reload.content.should == 'updated code' end + + it "should update an existing project snippet with new title" do + put api("/projects/#{project.id}/snippets/#{snippet.id}", user), + title: 'other api test' + response.status.should == 200 + json_response['title'].should == 'other api test' + end end describe "DELETE /projects/:id/snippets/:snippet_id" do @@ -397,6 +592,12 @@ describe Gitlab::API do expect { delete api("/projects/#{project.id}/snippets/#{snippet.id}", user) }.to change { Snippet.count }.by(-1) + response.status.should == 200 + end + + it "should return success when deleting unknown snippet id" do + delete api("/projects/#{project.id}/snippets/1234", user) + response.status.should == 200 end end @@ -405,9 +606,14 @@ describe Gitlab::API do get api("/projects/#{project.id}/snippets/#{snippet.id}/raw", user) response.status.should == 200 end + + it "should return a 404 error if raw project snippet not found" do + get api("/projects/#{project.id}/snippets/5555/raw", user) + response.status.should == 404 + end end - describe "GET /projects/:id/:sha/blob" do + describe "GET /projects/:id/repository/commits/:sha/blob" do it "should get the raw file contents" do get api("/projects/#{project.id}/repository/commits/master/blob?filepath=README.md", user) response.status.should == 200 @@ -422,6 +628,11 @@ describe Gitlab::API do get api("/projects/#{project.id}/repository/commits/master/blob?filepath=README.invalid", user) response.status.should == 404 end + + it "should return a 400 error if filepath is missing" do + get api("/projects/#{project.id}/repository/commits/master/blob", user) + response.status.should == 400 + end end describe "GET /projects/:id/keys" do diff --git a/spec/requests/api/session_spec.rb b/spec/requests/api/session_spec.rb index afae8be8..2cdb0d7e 100644 --- a/spec/requests/api/session_spec.rb +++ b/spec/requests/api/session_spec.rb @@ -35,5 +35,15 @@ describe Gitlab::API do json_response['private_token'].should be_nil end end + + context "when empty name" do + it "should return authentication error" do + post api("/session"), password: user.password + response.status.should == 401 + + json_response['email'].should be_nil + json_response['private_token'].should be_nil + end + end end end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index e6ac892d..c2c9f846 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -31,26 +31,69 @@ describe Gitlab::API do response.status.should == 200 json_response['email'].should == user.email end + + it "should return a 401 if unauthenticated" do + get api("/users/9998") + response.status.should == 401 + end + + it "should return a 404 error if user id not found" do + get api("/users/9999", user) + response.status.should == 404 + end end describe "POST /users" do before{ admin } - it "should not create invalid user" do - post api("/users", admin), { email: "invalid email" } - response.status.should == 404 - end - it "should create user" do expect { post api("/users", admin), attributes_for(:user, projects_limit: 3) }.to change { User.count }.by(1) end + it "should return 201 Created on success" do + post api("/users", admin), attributes_for(:user, projects_limit: 3) + response.status.should == 201 + end + + it "should not create user with invalid email" do + post api("/users", admin), { email: "invalid email", password: 'password' } + response.status.should == 400 + end + + it "should return 400 error if password not given" do + post api("/users", admin), { email: 'test@example.com' } + response.status.should == 400 + end + + it "should return 400 error if email not given" do + post api("/users", admin), { password: 'pass1234' } + response.status.should == 400 + end + it "shouldn't available for non admin users" do post api("/users", user), attributes_for(:user) response.status.should == 403 end + + context "with existing user" do + before { post api("/users", admin), { email: 'test@example.com', password: 'password', username: 'test' } } + + it "should not create user with same email" do + expect { + post api("/users", admin), { email: 'test@example.com', password: 'password' } + }.to change { User.count }.by(0) + end + + it "should return 409 conflict error if user with email exists" do + post api("/users", admin), { email: 'test@example.com', password: 'password' } + end + + it "should return 409 conflict error if same username exists" do + post api("/users", admin), { email: 'foo@example.com', password: 'pass', username: 'test' } + end + end end describe "GET /users/sign_up" do @@ -81,7 +124,7 @@ describe Gitlab::API do describe "PUT /users/:id" do before { admin } - it "should update user" do + it "should update user with new bio" do put api("/users/#{user.id}", admin), {bio: 'new test bio'} response.status.should == 200 json_response['bio'].should == 'new test bio' @@ -103,6 +146,25 @@ describe Gitlab::API do put api("/users/999999", admin), {bio: 'update should fail'} response.status.should == 404 end + + context "with existing user" do + before { + post api("/users", admin), { email: 'test@example.com', password: 'password', username: 'test', name: 'test' } + post api("/users", admin), { email: 'foo@bar.com', password: 'password', username: 'john', name: 'john' } + @user_id = User.all.last.id + } + +# it "should return 409 conflict error if email address exists" do +# put api("/users/#{@user_id}", admin), { email: 'test@example.com' } +# response.status.should == 409 +# end +# +# it "should return 409 conflict error if username taken" do +# @user_id = User.all.last.id +# put api("/users/#{@user_id}", admin), { username: 'test' } +# response.status.should == 409 +# end + end end describe "POST /users/:id/keys" do @@ -131,6 +193,11 @@ describe Gitlab::API do json_response['email'].should == user.email end + it "should not delete for unauthenticated user" do + delete api("/users/#{user.id}") + response.status.should == 401 + end + it "shouldn't available for non admin users" do delete api("/users/#{user.id}", user) response.status.should == 403 @@ -148,6 +215,11 @@ describe Gitlab::API do response.status.should == 200 json_response['email'].should == user.email end + + it "should return 401 error if user is unauthenticated" do + get api("/user") + response.status.should == 401 + end end describe "GET /user/keys" do @@ -183,19 +255,38 @@ describe Gitlab::API do get api("/user/keys/42", user) response.status.should == 404 end + + it "should return 404 error if admin accesses user's ssh key" do + user.keys << key + user.save + admin + get api("/user/keys/#{key.id}", admin) + response.status.should == 404 + end end describe "POST /user/keys" do - it "should not create invalid ssh key" do - post api("/user/keys", user), { title: "invalid key" } - response.status.should == 404 - end - it "should create ssh key" do key_attrs = attributes_for :key expect { post api("/user/keys", user), key_attrs }.to change{ user.keys.count }.by(1) + response.status.should == 201 + end + + it "should return a 401 error if unauthorized" do + post api("/user/keys"), title: 'some title', key: 'some key' + response.status.should == 401 + end + + it "should not create ssh key without key" do + post api("/user/keys", user), title: 'title' + response.status.should == 400 + end + + it "should not create ssh key without title" do + post api("/user/keys", user), key: "somekey" + response.status.should == 400 end end @@ -206,11 +297,19 @@ describe Gitlab::API do expect { delete api("/user/keys/#{key.id}", user) }.to change{user.keys.count}.by(-1) + response.status.should == 200 end - it "should return 404 Not Found within invalid ID" do + it "should return sucess if key ID not found" do delete api("/user/keys/42", user) - response.status.should == 404 + response.status.should == 200 + end + + it "should return 401 error if unauthorized" do + user.keys << key + user.save + delete api("/user/keys/#{key.id}") + response.status.should == 401 end end end