Commit a7055be1fdecc51afc4e8f0e94267fcd9d9ef0c1

Authored by Dmitriy Zaporozhets
2 parents d2cec126 ecf53bb9

Merge pull request #2835 from Asquera/fixes/api

Fix API return codes
app/models/merge_request.rb
... ... @@ -91,7 +91,7 @@ class MergeRequest < ActiveRecord::Base
91 91  
92 92 def validate_branches
93 93 if target_branch == source_branch
94   - errors.add :base, "You can not use same branch for source and target branches"
  94 + errors.add :branch_conflict, "You can not use same branch for source and target branches"
95 95 end
96 96 end
97 97  
... ...
app/models/project.rb
... ... @@ -160,7 +160,7 @@ class Project < ActiveRecord::Base
160 160  
161 161 def check_limit
162 162 unless creator.can_create_project?
163   - errors[:base] << ("Your own projects limit is #{creator.projects_limit}! Please contact administrator to increase it")
  163 + errors[:limit_reached] << ("Your own projects limit is #{creator.projects_limit}! Please contact administrator to increase it")
164 164 end
165 165 rescue
166 166 errors[:base] << ("Can't check your ability to create project")
... ...
config/routes.rb
... ... @@ -8,6 +8,7 @@ Gitlab::Application.routes.draw do
8 8  
9 9 # API
10 10 require 'api'
  11 + Gitlab::API.logger Rails.logger
11 12 mount Gitlab::API => '/api'
12 13  
13 14 constraint = lambda { |request| request.env["warden"].authenticate? and request.env['warden'].user.admin? }
... ...
doc/api/README.md
1 1 # GitLab API
2 2  
3   -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.
  3 +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.
4 4  
5 5 If no, or an invalid, `private_token` is provided then an error message will be returned with status code 401:
6 6  
... ... @@ -18,8 +18,48 @@ Example of a valid API request:
18 18 GET http://example.com/api/v3/projects?private_token=QVy1PB7sTxfy4pqfZM1U
19 19 ```
20 20  
  21 +Example for a valid API request using curl and authentication via header:
  22 +
  23 +```
  24 +curl --header "PRIVATE-TOKEN: QVy1PB7sTxfy4pqfZM1U" "http://example.com/api/v3/projects"
  25 +```
  26 +
  27 +
21 28 The API uses JSON to serialize data. You don't need to specify `.json` at the end of API URL.
22 29  
  30 +
  31 +
  32 +## Status codes
  33 +
  34 +The API is designed to return different status codes according to context and action. In this way
  35 +if a request results in an error the caller is able to get insight into what went wrong, e.g.
  36 +status code `400 Bad Request` is returned if a required attribute is missing from the request.
  37 +The following list gives an overview of how the API functions generally behave.
  38 +
  39 +API request types:
  40 +
  41 +* `GET` requests access one or more resources and return the result as JSON
  42 +* `POST` requests return `201 Created` if the resource is successfully created and return the newly created resource as JSON
  43 +* `GET`, `PUT` and `DELETE` return `200 Ok` if the resource is accessed, modified or deleted successfully, the (modified) result is returned as JSON
  44 +* `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.
  45 +
  46 +
  47 +The following list shows the possible return codes for API requests.
  48 +
  49 +Return values:
  50 +
  51 +* `200 Ok` - The `GET`, `PUT` or `DELETE` request was successful, the resource(s) itself is returned as JSON
  52 +* `201 Created` - The `POST` request was successful and the resource is returned as JSON
  53 +* `400 Bad Request` - A required attribute of the API request is missing, e.g. the title of an issue is not given
  54 +* `401 Unauthorized` - The user is not authenticated, a valid user token is necessary, see above
  55 +* `403 Forbidden` - The request is not allowed, e.g. the user is not allowed to delete a project
  56 +* `404 Not Found` - A resource could not be accessed, e.g. an ID for a resource could not be found
  57 +* `405 Method Not Allowed` - The request is not supported
  58 +* `409 Conflict` - A conflicting resource already exists, e.g. creating a project with a name that already exists
  59 +* `500 Server Error` - While handling the request something went wrong on the server side
  60 +
  61 +
  62 +
23 63 #### Pagination
24 64  
25 65 When listing resources you can pass the following parameters:
... ...
doc/api/groups.md
... ... @@ -17,7 +17,8 @@ GET /groups
17 17 ]
18 18 ```
19 19  
20   -## Details of group
  20 +
  21 +## Details of a group
21 22  
22 23 Get all details of a group.
23 24  
... ... @@ -29,19 +30,19 @@ Parameters:
29 30  
30 31 + `id` (required) - The ID of a group
31 32  
  33 +
32 34 ## New group
33 35  
34   -Create a new project group. Available only for admin
  36 +Creates a new project group. Available only for admin.
35 37  
36 38 ```
37 39 POST /groups
38 40 ```
39 41  
40 42 Parameters:
41   -+ `name` (required) - Email
42   -+ `path` - Password
43 43  
44   -Will return created group with status `201 Created` on success, or `404 Not found` on fail.
  44 ++ `name` (required) - The name of the group
  45 ++ `path` (required) - The path of the group
45 46  
46 47 ## Transfer project to group
47 48  
... ...
doc/api/issues.md
1 1 ## List issues
2 2  
3   -Get all issues created by authenticed user.
  3 +Get all issues created by authenticed user. This function takes pagination parameters
  4 +`page` and `per_page` to restrict the list of issues.
4 5  
5 6 ```
6 7 GET /issues
... ... @@ -68,9 +69,11 @@ GET /issues
68 69 ]
69 70 ```
70 71  
  72 +
71 73 ## List project issues
72 74  
73   -Get a list of project issues.
  75 +Get a list of project issues. This function accepts pagination parameters `page` and `per_page`
  76 +to return the list of project issues.
74 77  
75 78 ```
76 79 GET /projects/:id/issues
... ... @@ -80,9 +83,10 @@ Parameters:
80 83  
81 84 + `id` (required) - The ID of a project
82 85  
  86 +
83 87 ## Single issue
84 88  
85   -Get a project issue.
  89 +Gets a single project issue.
86 90  
87 91 ```
88 92 GET /projects/:id/issues/:issue_id
... ... @@ -133,9 +137,10 @@ Parameters:
133 137 }
134 138 ```
135 139  
  140 +
136 141 ## New issue
137 142  
138   -Create a new project issue.
  143 +Creates a new project issue.
139 144  
140 145 ```
141 146 POST /projects/:id/issues
... ... @@ -150,11 +155,10 @@ Parameters:
150 155 + `milestone_id` (optional) - The ID of a milestone to assign issue
151 156 + `labels` (optional) - Comma-separated label names for an issue
152 157  
153   -Will return created issue with status `201 Created` on success, or `404 Not found` on fail.
154 158  
155 159 ## Edit issue
156 160  
157   -Update an existing project issue.
  161 +Updates an existing project issue. This function is also used to mark an issue as closed.
158 162  
159 163 ```
160 164 PUT /projects/:id/issues/:issue_id
... ... @@ -171,5 +175,19 @@ Parameters:
171 175 + `labels` (optional) - Comma-separated label names for an issue
172 176 + `closed` (optional) - The state of an issue (0 = false, 1 = true)
173 177  
174   -Will return updated issue with status `200 OK` on success, or `404 Not found` on fail.
  178 +
  179 +## Delete existing issue (**Deprecated**)
  180 +
  181 +The function is deprecated and returns a `405 Method Not Allowed`
  182 +error if called. An issue gets now closed and is done by calling `PUT /projects/:id/issues/:issue_id` with
  183 +parameter `closed` set to 1.
  184 +
  185 +```
  186 +DELETE /projects/:id/issues/:issue_id
  187 +```
  188 +
  189 +Parameters:
  190 +
  191 ++ `id` (required) - The project ID
  192 ++ `issue_id` (required) - The ID of the issue
175 193  
... ...
doc/api/merge_requests.md
1 1 ## List merge requests
2 2  
3   -Get all MR for this project.
  3 +Get all merge requests for this project. This function takes pagination parameters
  4 +`page` and `per_page` to restrict the list of merge requests.
4 5  
5 6 ```
6 7 GET /projects/:id/merge_requests
... ... @@ -40,9 +41,10 @@ Parameters:
40 41 ]
41 42 ```
42 43  
43   -## Show MR
44 44  
45   -Show information about MR.
  45 +## Get single MR
  46 +
  47 +Shows information about a single merge request.
46 48  
47 49 ```
48 50 GET /projects/:id/merge_request/:merge_request_id
... ... @@ -84,7 +86,7 @@ Parameters:
84 86  
85 87 ## Create MR
86 88  
87   -Create MR.
  89 +Creates a new merge request.
88 90  
89 91 ```
90 92 POST /projects/:id/merge_requests
... ... @@ -126,9 +128,10 @@ Parameters:
126 128 }
127 129 ```
128 130  
  131 +
129 132 ## Update MR
130 133  
131   -Update MR. You can change branches, title, or even close the MR.
  134 +Updates an existing merge request. You can change branches, title, or even close the MR.
132 135  
133 136 ```
134 137 PUT /projects/:id/merge_request/:merge_request_id
... ... @@ -172,9 +175,11 @@ Parameters:
172 175 }
173 176 }
174 177 ```
  178 +
  179 +
175 180 ## Post comment to MR
176 181  
177   -Post comment to MR
  182 +Adds a comment to a merge request.
178 183  
179 184 ```
180 185 POST /projects/:id/merge_request/:merge_request_id/comments
... ... @@ -183,10 +188,9 @@ POST /projects/:id/merge_request/:merge_request_id/comments
183 188 Parameters:
184 189  
185 190 + `id` (required) - The ID of a project
186   -+ `merge_request_id` (required) - ID of MR
  191 ++ `merge_request_id` (required) - ID of merge request
187 192 + `note` (required) - Text of comment
188 193  
189   -Will return created note with status `201 Created` on success, or `404 Not found` on fail.
190 194  
191 195 ```json
192 196 {
... ...
doc/api/milestones.md
1 1 ## List project milestones
2 2  
3   -Get a list of project milestones.
  3 +Returns a list of project milestones.
4 4  
5 5 ```
6 6 GET /projects/:id/milestones
... ... @@ -10,9 +10,10 @@ Parameters:
10 10  
11 11 + `id` (required) - The ID of a project
12 12  
13   -## Single milestone
14 13  
15   -Get a single project milestone.
  14 +## Get single milestone
  15 +
  16 +Gets a single project milestone.
16 17  
17 18 ```
18 19 GET /projects/:id/milestones/:milestone_id
... ... @@ -23,9 +24,10 @@ Parameters:
23 24 + `id` (required) - The ID of a project
24 25 + `milestone_id` (required) - The ID of a project milestone
25 26  
26   -## New milestone
27 27  
28   -Create a new project milestone.
  28 +## Create new milestone
  29 +
  30 +Creates a new project milestone.
29 31  
30 32 ```
31 33 POST /projects/:id/milestones
... ... @@ -38,9 +40,10 @@ Parameters:
38 40 + `description` (optional) - The description of the milestone
39 41 + `due_date` (optional) - The due date of the milestone
40 42  
  43 +
41 44 ## Edit milestone
42 45  
43   -Update an existing project milestone.
  46 +Updates an existing project milestone.
44 47  
45 48 ```
46 49 PUT /projects/:id/milestones/:milestone_id
... ... @@ -54,3 +57,4 @@ Parameters:
54 57 + `description` (optional) - The description of a milestone
55 58 + `due_date` (optional) - The due date of the milestone
56 59 + `closed` (optional) - The status of the milestone
  60 +
... ...
doc/api/notes.md
1   -## List notes
  1 +## Wall
2 2  
3 3 ### List project wall notes
4 4  
... ... @@ -30,22 +30,40 @@ Parameters:
30 30  
31 31 + `id` (required) - The ID of a project
32 32  
33   -### List merge request notes
34 33  
35   -Get a list of merge request notes.
  34 +### Get single wall note
  35 +
  36 +Returns a single wall note.
36 37  
37 38 ```
38   -GET /projects/:id/merge_requests/:merge_request_id/notes
  39 +GET /projects/:id/notes/:note_id
39 40 ```
40 41  
41 42 Parameters:
42 43  
43 44 + `id` (required) - The ID of a project
44   -+ `merge_request_id` (required) - The ID of an merge request
  45 ++ `note_id` (required) - The ID of a wall note
45 46  
46   -### List issue notes
47 47  
48   -Get a list of issue notes.
  48 +### Create new wall note
  49 +
  50 +Creates a new wall note.
  51 +
  52 +```
  53 +POST /projects/:id/notes
  54 +```
  55 +
  56 +Parameters:
  57 +
  58 ++ `id` (required) - The ID of a project
  59 ++ `body` (required) - The content of a note
  60 +
  61 +
  62 +## Issues
  63 +
  64 +### List project issue notes
  65 +
  66 +Gets a list of all notes for a single issue.
49 67  
50 68 ```
51 69 GET /projects/:id/issues/:issue_id/notes
... ... @@ -56,54 +74,59 @@ Parameters:
56 74 + `id` (required) - The ID of a project
57 75 + `issue_id` (required) - The ID of an issue
58 76  
59   -### List snippet notes
60 77  
61   -Get a list of snippet notes.
  78 +### Get single issue note
  79 +
  80 +Returns a single note for a specific project issue
62 81  
63 82 ```
64   -GET /projects/:id/snippets/:snippet_id/notes
  83 +GET /projects/:id/issues/:issue_id/notes/:note_id
65 84 ```
66 85  
67 86 Parameters:
68 87  
69 88 + `id` (required) - The ID of a project
70   -+ `snippet_id` (required) - The ID of a snippet
  89 ++ `issue_id` (required) - The ID of a project issue
  90 ++ `note_id` (required) - The ID of an issue note
71 91  
72   -## Single note
73 92  
74   -### Single wall note
  93 +### Create new issue note
75 94  
76   -Get a wall note.
  95 +Creates a new note to a single project issue.
77 96  
78 97 ```
79   -GET /projects/:id/notes/:note_id
  98 +POST /projects/:id/issues/:issue_id/notes
80 99 ```
81 100  
82 101 Parameters:
83 102  
84 103 + `id` (required) - The ID of a project
85   -+ `note_id` (required) - The ID of a wall note
  104 ++ `issue_id` (required) - The ID of an issue
  105 ++ `body` (required) - The content of a note
86 106  
87   -### Single issue note
88 107  
89   -Get an issue note.
  108 +## Snippets
  109 +
  110 +### List all snippet notes
  111 +
  112 +Gets a list of all notes for a single snippet. Snippet notes are comments users can post to a snippet.
90 113  
91 114 ```
92   -GET /projects/:id/issues/:issue_id/:notes/:note_id
  115 +GET /projects/:id/snippets/:snippet_id/notes
93 116 ```
94 117  
95 118 Parameters:
96 119  
97 120 + `id` (required) - The ID of a project
98   -+ `issue_id` (required) - The ID of a project issue
99   -+ `note_id` (required) - The ID of an issue note
  121 ++ `snippet_id` (required) - The ID of a project snippet
  122 +
100 123  
101   -### Single snippet note
  124 +### Get single snippet note
102 125  
103   -Get a snippet note.
  126 +Returns a single note for a given snippet.
104 127  
105 128 ```
106   -GET /projects/:id/issues/:snippet_id/:notes/:note_id
  129 +GET /projects/:id/snippets/:snippet_id/notes/:note_id
107 130 ```
108 131  
109 132 Parameters:
... ... @@ -112,52 +135,64 @@ Parameters:
112 135 + `snippet_id` (required) - The ID of a project snippet
113 136 + `note_id` (required) - The ID of an snippet note
114 137  
115   -## New note
116 138  
117   -### New wall note
  139 +### Create new snippet note
118 140  
119   -Create a new wall note.
  141 +Creates a new note for a single snippet. Snippet notes are comments users can post to a snippet.
120 142  
121 143 ```
122   -POST /projects/:id/notes
  144 +POST /projects/:id/snippets/:snippet_id/notes
123 145 ```
124 146  
125 147 Parameters:
126 148  
127 149 + `id` (required) - The ID of a project
  150 ++ `snippet_id` (required) - The ID of an snippet
128 151 + `body` (required) - The content of a note
129 152  
130   -Will return created note with status `201 Created` on success, or `404 Not found` on fail.
131 153  
  154 +## Merge Requests
132 155  
133   -### New issue note
  156 +### List all merge request notes
134 157  
135   -Create a new issue note.
  158 +Gets a list of all notes for a single merge request.
136 159  
137 160 ```
138   -POST /projects/:id/issues/:issue_id/notes
  161 +GET /projects/:id/merge_requests/:merge_request_id/notes
139 162 ```
140 163  
141 164 Parameters:
142 165  
143 166 + `id` (required) - The ID of a project
144   -+ `issue_id` (required) - The ID of an issue
145   -+ `body` (required) - The content of a note
  167 ++ `merge_request_id` (required) - The ID of a project merge request
146 168  
147   -Will return created note with status `201 Created` on success, or `404 Not found` on fail.
148 169  
149   -### New snippet note
  170 +### Get single merge request note
150 171  
151   -Create a new snippet note.
  172 +Returns a single note for a given merge request.
152 173  
153 174 ```
154   -POST /projects/:id/snippets/:snippet_id/notes
  175 +GET /projects/:id/merge_requests/:merge_request_id/notes/:note_id
155 176 ```
156 177  
157 178 Parameters:
158 179  
159 180 + `id` (required) - The ID of a project
160   -+ `snippet_id` (required) - The ID of an snippet
  181 ++ `merge_request_id` (required) - The ID of a project merge request
  182 ++ `note_id` (required) - The ID of a merge request note
  183 +
  184 +
  185 +### Create new merge request note
  186 +
  187 +Creates a new note for a single merge request.
  188 +
  189 +```
  190 +POST /projects/:id/merge_requests/:merge_request_id/notes
  191 +```
  192 +
  193 +Parameters:
  194 +
  195 ++ `id` (required) - The ID of a project
  196 ++ `merge_request_id` (required) - The ID of a merge request
161 197 + `body` (required) - The content of a note
162 198  
163   -Will return created note with status `201 Created` on success, or `404 Not found` on fail.
... ...
doc/api/projects.md
1   -## List projects
  1 +## Projects
  2 +
  3 +### List projects
2 4  
3 5 Get a list of projects owned by the authenticated user.
4 6  
... ... @@ -55,9 +57,11 @@ GET /projects
55 57 ]
56 58 ```
57 59  
58   -## Single project
59 60  
60   -Get a specific project, identified by project ID, which is owned by the authentication user.
  61 +### Get single project
  62 +
  63 +Get a specific project, identified by project ID or NAME, which is owned by the authentication user.
  64 +Currently namespaced projects cannot retrieved by name.
61 65  
62 66 ```
63 67 GET /projects/:id
... ... @@ -65,7 +69,7 @@ GET /projects/:id
65 69  
66 70 Parameters:
67 71  
68   -+ `id` (required) - The ID of a project
  72 ++ `id` (required) - The ID or NAME of a project
69 73  
70 74 ```json
71 75 {
... ... @@ -92,9 +96,10 @@ Parameters:
92 96 }
93 97 ```
94 98  
95   -## Create project
96 99  
97   -Create new project owned by user
  100 +### Create project
  101 +
  102 +Creates new project owned by user.
98 103  
99 104 ```
100 105 POST /projects
... ... @@ -110,12 +115,21 @@ Parameters:
110 115 + `merge_requests_enabled` (optional) - enabled by default
111 116 + `wiki_enabled` (optional) - enabled by default
112 117  
113   -Will return created project with status `201 Created` on success, or `404 Not
114   -found` on fail.
  118 +**Project access levels**
115 119  
116   -## Create project for user
  120 +The project access levels are defined in the `user_project.rb` class. Currently, these levels are recoginized:
  121 +
  122 +```
  123 + GUEST = 10
  124 + REPORTER = 20
  125 + DEVELOPER = 30
  126 + MASTER = 40
  127 +```
117 128  
118   -Create new project owned by user. Available only for admin
  129 +
  130 +### Create project for user
  131 +
  132 +Creates a new project owned by user. Available only for admins.
119 133  
120 134 ```
121 135 POST /projects/user/:user_id
... ... @@ -132,10 +146,11 @@ Parameters:
132 146 + `merge_requests_enabled` (optional) - enabled by default
133 147 + `wiki_enabled` (optional) - enabled by default
134 148  
135   -Will return created project with status `201 Created` on success, or `404 Not
136   -found` on fail.
137 149  
138   -## List project team members
  150 +
  151 +## Team members
  152 +
  153 +### List project team members
139 154  
140 155 Get a list of project team members.
141 156  
... ... @@ -145,12 +160,13 @@ GET /projects/:id/members
145 160  
146 161 Parameters:
147 162  
148   -+ `id` (required) - The ID of a project
149   -+ `query` - Query string
  163 ++ `id` (required) - The ID or NAME of a project
  164 ++ `query` (optional) - Query string to search for members
  165 +
150 166  
151   -## Get project team member
  167 +### Get project team member
152 168  
153   -Get a project team member.
  169 +Gets a project team member.
154 170  
155 171 ```
156 172 GET /projects/:id/members/:user_id
... ... @@ -158,12 +174,11 @@ GET /projects/:id/members/:user_id
158 174  
159 175 Parameters:
160 176  
161   -+ `id` (required) - The ID of a project
  177 ++ `id` (required) - The ID or NAME of a project
162 178 + `user_id` (required) - The ID of a user
163 179  
164 180 ```json
165 181 {
166   -
167 182 "id": 1,
168 183 "username": "john_smith",
169 184 "email": "john@example.com",
... ... @@ -174,9 +189,12 @@ Parameters:
174 189 }
175 190 ```
176 191  
177   -## Add project team member
178 192  
179   -Add a user to a project team.
  193 +### Add project team member
  194 +
  195 +Adds a user to a project team. This is an idempotent method and can be called multiple times
  196 +with the same parameters. Adding team membership to a user that is already a member does not
  197 +affect the existing membership.
180 198  
181 199 ```
182 200 POST /projects/:id/members
... ... @@ -184,15 +202,14 @@ POST /projects/:id/members
184 202  
185 203 Parameters:
186 204  
187   -+ `id` (required) - The ID of a project
  205 ++ `id` (required) - The ID or NAME of a project
188 206 + `user_id` (required) - The ID of a user to add
189 207 + `access_level` (required) - Project access level
190 208  
191   -Will return status `201 Created` on success, or `404 Not found` on fail.
192 209  
193   -## Edit project team member
  210 +### Edit project team member
194 211  
195   -Update project team member to specified access level.
  212 +Updates project team member to a specified access level.
196 213  
197 214 ```
198 215 PUT /projects/:id/members/:user_id
... ... @@ -200,13 +217,12 @@ PUT /projects/:id/members/:user_id
200 217  
201 218 Parameters:
202 219  
203   -+ `id` (required) - The ID of a project
  220 ++ `id` (required) - The ID or NAME of a project
204 221 + `user_id` (required) - The ID of a team member
205 222 + `access_level` (required) - Project access level
206 223  
207   -Will return status `200 OK` on success, or `404 Not found` on fail.
208 224  
209   -## Remove project team member
  225 +### Remove project team member
210 226  
211 227 Removes user from project team.
212 228  
... ... @@ -216,14 +232,20 @@ DELETE /projects/:id/members/:user_id
216 232  
217 233 Parameters:
218 234  
219   -+ `id` (required) - The ID of a project
  235 ++ `id` (required) - The ID or NAME of a project
220 236 + `user_id` (required) - The ID of a team member
221 237  
222   -Status code `200` will be returned on success.
  238 +This method is idempotent and can be called multiple times with the same parameters.
  239 +Revoking team membership for a user who is not currently a team member is considered success.
  240 +Please note that the returned JSON currently differs slightly. Thus you should not
  241 +rely on the returned JSON structure.
  242 +
223 243  
224   -## List project hooks
  244 +## Hooks
225 245  
226   -Get list for project hooks
  246 +### List project hooks
  247 +
  248 +Get list of project hooks.
227 249  
228 250 ```
229 251 GET /projects/:id/hooks
... ... @@ -231,13 +253,12 @@ GET /projects/:id/hooks
231 253  
232 254 Parameters:
233 255  
234   -+ `id` (required) - The ID of a project
  256 ++ `id` (required) - The ID or NAME of a project
235 257  
236   -Will return hooks with status `200 OK` on success, or `404 Not found` on fail.
237 258  
238   -## Get project hook
  259 +### Get project hook
239 260  
240   -Get hook for project
  261 +Get a specific hook for project.
241 262  
242 263 ```
243 264 GET /projects/:id/hooks/:hook_id
... ... @@ -245,14 +266,21 @@ GET /projects/:id/hooks/:hook_id
245 266  
246 267 Parameters:
247 268  
248   -+ `id` (required) - The ID of a project
  269 ++ `id` (required) - The ID or NAME of a project
249 270 + `hook_id` (required) - The ID of a project hook
250 271  
251   -Will return hook with status `200 OK` on success, or `404 Not found` on fail.
  272 +```json
  273 +{
  274 + "id": 1,
  275 + "url": "http://example.com/hook",
  276 + "created_at": "2012-10-12T17:04:47Z"
  277 +}
  278 +```
252 279  
253   -## Add project hook
254 280  
255   -Add hook to project
  281 +### Add project hook
  282 +
  283 +Adds a hook to project.
256 284  
257 285 ```
258 286 POST /projects/:id/hooks
... ... @@ -260,14 +288,13 @@ POST /projects/:id/hooks
260 288  
261 289 Parameters:
262 290  
263   -+ `id` (required) - The ID of a project
  291 ++ `id` (required) - The ID or NAME of a project
264 292 + `url` (required) - The hook URL
265 293  
266   -Will return status `201 Created` on success, or `404 Not found` on fail.
267 294  
268   -## Edit project hook
  295 +### Edit project hook
269 296  
270   -Edit hook for project
  297 +Edits a hook for project.
271 298  
272 299 ```
273 300 PUT /projects/:id/hooks/:hook_id
... ... @@ -275,30 +302,125 @@ PUT /projects/:id/hooks/:hook_id
275 302  
276 303 Parameters:
277 304  
278   -+ `id` (required) - The ID of a project
  305 ++ `id` (required) - The ID or NAME of a project
279 306 + `hook_id` (required) - The ID of a project hook
280 307 + `url` (required) - The hook URL
281 308  
282   -Will return status `201 Created` on success, or `404 Not found` on fail.
283   -
284 309  
285   -## Delete project hook
  310 +### Delete project hook
286 311  
287   -Delete hook from project
  312 +Removes a hook from project. This is an idempotent method and can be called multiple times.
  313 +Either the hook is available or not.
288 314  
289 315 ```
290   -DELETE /projects/:id/hooks/:hook_id
  316 +DELETE /projects/:id/hooks/
291 317 ```
292 318  
293 319 Parameters:
294 320  
295   -+ `id` (required) - The ID of a project
  321 ++ `id` (required) - The ID or NAME of a project
296 322 + `hook_id` (required) - The ID of hook to delete
297 323  
298   -Will return status `200 OK` on success, or `404 Not found` on fail.
  324 +Note the JSON response differs if the hook is available or not. If the project hook
  325 +is available before it is returned in the JSON response or an empty response is returned.
299 326  
300 327  
301   -## List deploy keys
  328 +## Branches
  329 +
  330 +### List branches
  331 +
  332 +Lists all branches of a project.
  333 +
  334 +```
  335 +GET /projects/:id/repository/branches
  336 +```
  337 +
  338 +Parameters:
  339 +
  340 ++ `id` (required) - The ID of the project
  341 +
  342 +
  343 +### List single branch
  344 +
  345 +Lists a specific branch of a project.
  346 +
  347 +```
  348 +GET /projects/:id/repository/branches/:branch
  349 +```
  350 +
  351 +Parameters:
  352 +
  353 ++ `id` (required) - The ID of the project.
  354 ++ `branch` (required) - The name of the branch.
  355 +
  356 +
  357 +### Protect single branch
  358 +
  359 +Protects a single branch of a project.
  360 +
  361 +```
  362 +PUT /projects/:id/repository/branches/:branch/protect
  363 +```
  364 +
  365 +Parameters:
  366 +
  367 ++ `id` (required) - The ID of the project.
  368 ++ `branch` (required) - The name of the branch.
  369 +
  370 +
  371 +### Unprotect single branch
  372 +
  373 +Unprotects a single branch of a project.
  374 +
  375 +```
  376 +PUT /projects/:id/repository/branches/:branch/unprotect
  377 +```
  378 +
  379 +Parameters:
  380 +
  381 ++ `id` (required) - The ID of the project.
  382 ++ `branch` (required) - The name of the branch.
  383 +
  384 +
  385 +### List tags
  386 +
  387 +Lists all tags of a project.
  388 +
  389 +```
  390 +GET /projects/:id/repository/tags
  391 +```
  392 +
  393 +Parameters:
  394 +
  395 ++ `id` (required) - The ID of the project
  396 +
  397 +
  398 +### List commits
  399 +
  400 +Lists all commits with pagination. If the optional `ref_name` name is not given the commits of
  401 +the default branch (usually master) are returned.
  402 +
  403 +```
  404 +GET /projects/:id/repository/commits
  405 +```
  406 +
  407 +Parameters:
  408 +
  409 ++ `id` (required) - The Id of the project
  410 ++ `ref_name` (optional) - The name of a repository branch or tag
  411 ++ `page` (optional) - The page of commits to return (`0` default)
  412 ++ `per_page` (optional) - The number of commits per page (`20` default)
  413 +
  414 +Returns values:
  415 +
  416 ++ `200 Ok` on success and a list with commits
  417 ++ `404 Not Found` if project with id or the branch with `ref_name` not found
  418 +
  419 +
  420 +
  421 +## Deploy Keys
  422 +
  423 +### List deploy keys
302 424  
303 425 Get a list of a project's deploy keys.
304 426  
... ... @@ -306,6 +428,10 @@ Get a list of a project&#39;s deploy keys.
306 428 GET /projects/:id/keys
307 429 ```
308 430  
  431 +Parameters:
  432 +
  433 ++ `id` (required) - The ID of the project
  434 +
309 435 ```json
310 436 [
311 437 {
... ... @@ -325,7 +451,8 @@ GET /projects/:id/keys
325 451 ]
326 452 ```
327 453  
328   -## Single deploy key
  454 +
  455 +### Single deploy key
329 456  
330 457 Get a single key.
331 458  
... ... @@ -335,7 +462,8 @@ GET /projects/:id/keys/:key_id
335 462  
336 463 Parameters:
337 464  
338   -+ `id` (required) - The ID of an deploy key
  465 ++ `id` (required) - The ID of the project
  466 ++ `key_id` (required) - The ID of the deploy key
339 467  
340 468 ```json
341 469 {
... ... @@ -346,9 +474,11 @@ Parameters:
346 474 soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0="
347 475 }
348 476 ```
349   -## Add deploy key
350 477  
351   -Create new deploy key for a project
  478 +
  479 +### Add deploy key
  480 +
  481 +Creates a new deploy key for a project.
352 482  
353 483 ```
354 484 POST /projects/:id/keys
... ... @@ -356,13 +486,12 @@ POST /projects/:id/keys
356 486  
357 487 Parameters:
358 488  
359   -+ `title` (required) - new deploy key's title
360   -+ `key` (required) - new deploy key
  489 ++ `id` (required) - The ID of the project
  490 ++ `title` (required) - New deploy key's title
  491 ++ `key` (required) - New deploy key
361 492  
362   -Will return created key with status `201 Created` on success, or `404 Not
363   -found` on fail.
364 493  
365   -## Delete deploy key
  494 +### Delete deploy key
366 495  
367 496 Delete a deploy key from a project
368 497  
... ... @@ -372,6 +501,6 @@ DELETE /projects/:id/keys/:key_id
372 501  
373 502 Parameters:
374 503  
375   -+ `id` (required) - Deploy key ID
  504 ++ `id` (required) - The ID of the project
  505 ++ `key_id` (required) - The ID of the deploy key
376 506  
377   -Will return `200 OK` on success, or `404 Not Found` on fail.
... ...
doc/api/repositories.md
1   -## Project repository branches
  1 +## List repository branches
2 2  
3 3 Get a list of repository branches from a project, sorted by name alphabetically.
4 4  
... ... @@ -39,7 +39,8 @@ Parameters:
39 39 ]
40 40 ```
41 41  
42   -## Project repository branch
  42 +
  43 +## Get single repository branch
43 44  
44 45 Get a single project repository branch.
45 46  
... ... @@ -79,12 +80,11 @@ Parameters:
79 80 }
80 81 ```
81 82  
82   -Will return status code `200` on success or `404 Not found` if the branch is not available.
83   -
84 83  
85   -## Protect a project repository branch
  84 +## Protect repository branch
86 85  
87   -Protect a single project repository branch.
  86 +Protects a single project repository branch. This is an idempotent function, protecting an already
  87 +protected repository branch still returns a `200 Ok` status code.
88 88  
89 89 ```
90 90 PUT /projects/:id/repository/branches/:branch/protect
... ... @@ -122,9 +122,11 @@ Parameters:
122 122 }
123 123 ```
124 124  
125   -## Unprotect a project repository branch
126 125  
127   -Unprotect a single project repository branch.
  126 +## Unprotect repository branch
  127 +
  128 +Unprotects a single project repository branch. This is an idempotent function, unprotecting an already
  129 +unprotected repository branch still returns a `200 Ok` status code.
128 130  
129 131 ```
130 132 PUT /projects/:id/repository/branches/:branch/unprotect
... ... @@ -162,7 +164,8 @@ Parameters:
162 164 }
163 165 ```
164 166  
165   -## Project repository tags
  167 +
  168 +## List project repository tags
166 169  
167 170 Get a list of repository tags from a project, sorted by name in reverse alphabetical order.
168 171  
... ... @@ -201,7 +204,8 @@ Parameters:
201 204 ]
202 205 ```
203 206  
204   -## Project repository commits
  207 +
  208 +## List repository commits
205 209  
206 210 Get a list of repository commits in a project.
207 211  
... ... @@ -212,7 +216,7 @@ GET /projects/:id/repository/commits
212 216 Parameters:
213 217  
214 218 + `id` (required) - The ID of a project
215   -+ `ref_name` (optional) - The name of a repository branch or tag
  219 ++ `ref_name` (optional) - The name of a repository branch or tag or if not given the default branch
216 220  
217 221 ```json
218 222 [
... ... @@ -235,6 +239,7 @@ Parameters:
235 239 ]
236 240 ```
237 241  
  242 +
238 243 ## Raw blob content
239 244  
240 245 Get the raw file contents for a file.
... ... @@ -248,5 +253,3 @@ Parameters:
248 253 + `id` (required) - The ID of a project
249 254 + `sha` (required) - The commit or branch name
250 255 + `filepath` (required) - The path the file
251   -
252   -Will return the raw file contents.
... ...
doc/api/snippets.md
... ... @@ -10,9 +10,10 @@ Parameters:
10 10  
11 11 + `id` (required) - The ID of a project
12 12  
  13 +
13 14 ## Single snippet
14 15  
15   -Get a project snippet.
  16 +Get a single project snippet.
16 17  
17 18 ```
18 19 GET /projects/:id/snippets/:snippet_id
... ... @@ -42,22 +43,10 @@ Parameters:
42 43 }
43 44 ```
44 45  
45   -## Snippet content
46   -
47   -Get a raw project snippet.
48   -
49   -```
50   -GET /projects/:id/snippets/:snippet_id/raw
51   -```
52   -
53   -Parameters:
54   -
55   -+ `id` (required) - The ID of a project
56   -+ `snippet_id` (required) - The ID of a project's snippet
57 46  
58   -## New snippet
  47 +## Create new snippet
59 48  
60   -Create a new project snippet.
  49 +Creates a new project snippet. The user must have permission to create new snippets.
61 50  
62 51 ```
63 52 POST /projects/:id/snippets
... ... @@ -71,11 +60,10 @@ Parameters:
71 60 + `lifetime` (optional) - The expiration date of a snippet
72 61 + `code` (required) - The content of a snippet
73 62  
74   -Will return created snippet with status `201 Created` on success, or `404 Not found` on fail.
75 63  
76   -## Edit snippet
  64 +## Update snippet
77 65  
78   -Update an existing project snippet.
  66 +Updates an existing project snippet. The user must have permission to change an existing snippet.
79 67  
80 68 ```
81 69 PUT /projects/:id/snippets/:snippet_id
... ... @@ -90,11 +78,11 @@ Parameters:
90 78 + `lifetime` (optional) - The expiration date of a snippet
91 79 + `code` (optional) - The content of a snippet
92 80  
93   -Will return updated snippet with status `200 OK` on success, or `404 Not found` on fail.
94 81  
95 82 ## Delete snippet
96 83  
97   -Delete existing project snippet.
  84 +Deletes an existing project snippet. This is an idempotent function and deleting a non-existent
  85 +snippet still returns a `200 Ok` status code.
98 86  
99 87 ```
100 88 DELETE /projects/:id/snippets/:snippet_id
... ... @@ -105,5 +93,16 @@ Parameters:
105 93 + `id` (required) - The ID of a project
106 94 + `snippet_id` (required) - The ID of a project's snippet
107 95  
108   -Status code `200` will be returned on success.
109 96  
  97 +## Snippet content
  98 +
  99 +Returns the raw project snippet as plain text.
  100 +
  101 +```
  102 +GET /projects/:id/snippets/:snippet_id/raw
  103 +```
  104 +
  105 +Parameters:
  106 +
  107 ++ `id` (required) - The ID of a project
  108 ++ `snippet_id` (required) - The ID of a project's snippet
... ...
doc/api/users.md
... ... @@ -43,6 +43,7 @@ GET /users
43 43 ]
44 44 ```
45 45  
  46 +
46 47 ## Single user
47 48  
48 49 Get a single user.
... ... @@ -74,37 +75,40 @@ Parameters:
74 75 }
75 76 ```
76 77  
  78 +
77 79 ## User creation
78   -Create user. Available only for admin
  80 +
  81 +Creates a new user. Note only administrators can create new users.
79 82  
80 83 ```
81 84 POST /users
82 85 ```
83 86  
84 87 Parameters:
85   -+ `email` (required) - Email
86   -+ `password` (required) - Password
87   -+ `username` (required) - Username
88   -+ `name` (required) - Name
89   -+ `skype` - Skype ID
90   -+ `linkedin` - Linkedin
91   -+ `twitter` - Twitter account
92   -+ `projects_limit` - Number of projects user can create
93   -+ `extern_uid` - External UID
94   -+ `provider` - External provider name
95   -+ `bio` - User's bio
96 88  
97   -Will return created user with status `201 Created` on success, or `404 Not
98   -found` on fail.
  89 ++ `email` (required) - Email
  90 ++ `password` (required) - Password
  91 ++ `username` (required) - Username
  92 ++ `name` (required) - Name
  93 ++ `skype` (optional) - Skype ID
  94 ++ `linkedin` (optional) - Linkedin
  95 ++ `twitter` (optional) - Twitter account
  96 ++ `projects_limit` (optional) - Number of projects user can create
  97 ++ `extern_uid` (optional) - External UID
  98 ++ `provider` (optional) - External provider name
  99 ++ `bio` (optional) - User's bio
  100 +
99 101  
100 102 ## User modification
101   -Modify user. Available only for admin
  103 +
  104 +Modifies an existing user. Only administrators can change attributes of a user.
102 105  
103 106 ```
104 107 PUT /users/:id
105 108 ```
106 109  
107 110 Parameters:
  111 +
108 112 + `email` - Email
109 113 + `username` - Username
110 114 + `name` - Name
... ... @@ -117,23 +121,28 @@ Parameters:
117 121 + `provider` - External provider name
118 122 + `bio` - User's bio
119 123  
  124 +Note, at the moment this method does only return a 404 error, even in cases where a 409 (Conflict) would
  125 +be more appropriate, e.g. when renaming the email address to some exsisting one.
120 126  
121   -Will return created user with status `200 OK` on success, or `404 Not
122   -found` on fail.
123 127  
124 128 ## User deletion
125   -Delete user. Available only for admin
  129 +
  130 +Deletes a user. Available only for administrators. This is an idempotent function, calling this function
  131 +for a non-existent user id still returns a status code `200 Ok`. The JSON response differs if the user
  132 +was actually deleted or not. In the former the user is returned and in the latter not.
126 133  
127 134 ```
128 135 DELETE /users/:id
129 136 ```
130 137  
131   -Will return deleted user with status `200 OK` on success, or `404 Not
132   -found` on fail.
  138 +Parameters:
  139 +
  140 ++ `id` (required) - The ID of the user
  141 +
133 142  
134 143 ## Current user
135 144  
136   -Get currently authenticated user.
  145 +Gets currently authenticated user.
137 146  
138 147 ```
139 148 GET /user
... ... @@ -156,6 +165,7 @@ GET /user
156 165 }
157 166 ```
158 167  
  168 +
159 169 ## List SSH keys
160 170  
161 171 Get a list of currently authenticated user's SSH keys.
... ... @@ -183,6 +193,11 @@ GET /user/keys
183 193 ]
184 194 ```
185 195  
  196 +Parameters:
  197 +
  198 ++ **none**
  199 +
  200 +
186 201 ## Single SSH key
187 202  
188 203 Get a single key.
... ... @@ -204,9 +219,11 @@ Parameters:
204 219 soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0="
205 220 }
206 221 ```
  222 +
  223 +
207 224 ## Add SSH key
208 225  
209   -Create new key owned by currently authenticated user
  226 +Creates a new key owned by the currently authenticated user.
210 227  
211 228 ```
212 229 POST /user/keys
... ... @@ -217,8 +234,6 @@ Parameters:
217 234 + `title` (required) - new SSH Key's title
218 235 + `key` (required) - new SSH key
219 236  
220   -Will return created key with status `201 Created` on success, or `404 Not
221   -found` on fail.
222 237  
223 238 ## Add SSH key for user
224 239  
... ... @@ -239,7 +254,8 @@ found` on fail.
239 254  
240 255 ## Delete SSH key
241 256  
242   -Delete key owned by currently authenticated user
  257 +Deletes key owned by currently authenticated user. This is an idempotent function and calling it on a key that is already
  258 +deleted or not available results in `200 Ok`.
243 259  
244 260 ```
245 261 DELETE /user/keys/:id
... ... @@ -249,4 +265,3 @@ Parameters:
249 265  
250 266 + `id` (required) - SSH key ID
251 267  
252   -Will return `200 OK` on success, or `404 Not Found` on fail.
... ...
lib/api.rb
... ... @@ -8,6 +8,19 @@ module Gitlab
8 8 rack_response({'message' => '404 Not found'}.to_json, 404)
9 9 end
10 10  
  11 + rescue_from :all do |exception|
  12 + # lifted from https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb#L60
  13 + # why is this not wrapped in something reusable?
  14 + trace = exception.backtrace
  15 +
  16 + message = "\n#{exception.class} (#{exception.message}):\n"
  17 + message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
  18 + message << " " << trace.join("\n ")
  19 +
  20 + API.logger.add Logger::FATAL, message
  21 + rack_response({'message' => '500 Internal Server Error'}, 500)
  22 + end
  23 +
11 24 format :json
12 25 helpers APIHelpers
13 26  
... ...
lib/api/groups.rb
... ... @@ -20,12 +20,14 @@ module Gitlab
20 20 # Create group. Available only for admin
21 21 #
22 22 # Parameters:
23   - # name (required) - Name
24   - # path (required) - Path
  23 + # name (required) - The name of the group
  24 + # path (required) - The path of the group
25 25 # Example Request:
26 26 # POST /groups
27 27 post do
28 28 authenticated_as_admin!
  29 + required_attributes! [:name, :path]
  30 +
29 31 attrs = attributes_for_keys [:name, :path]
30 32 @group = Group.new(attrs)
31 33 @group.owner = current_user
... ...
lib/api/helpers.rb
... ... @@ -41,6 +41,17 @@ module Gitlab
41 41 abilities.allowed?(object, action, subject)
42 42 end
43 43  
  44 + # Checks the occurrences of required attributes, each attribute must be present in the params hash
  45 + # or a Bad Request error is invoked.
  46 + #
  47 + # Parameters:
  48 + # keys (required) - A hash consisting of keys that must be present
  49 + def required_attributes!(keys)
  50 + keys.each do |key|
  51 + bad_request!(key) unless params[key].present?
  52 + end
  53 + end
  54 +
44 55 def attributes_for_keys(keys)
45 56 attrs = {}
46 57 keys.each do |key|
... ... @@ -55,6 +66,12 @@ module Gitlab
55 66 render_api_error!('403 Forbidden', 403)
56 67 end
57 68  
  69 + def bad_request!(attribute)
  70 + message = ["400 (Bad request)"]
  71 + message << "\"" + attribute.to_s + "\" not given"
  72 + render_api_error!(message.join(' '), 400)
  73 + end
  74 +
58 75 def not_found!(resource = nil)
59 76 message = ["404"]
60 77 message << resource if resource
... ...
lib/api/issues.rb
... ... @@ -48,6 +48,7 @@ module Gitlab
48 48 # Example Request:
49 49 # POST /projects/:id/issues
50 50 post ":id/issues" do
  51 + required_attributes! [:title]
51 52 attrs = attributes_for_keys [:title, :description, :assignee_id, :milestone_id]
52 53 attrs[:label_list] = params[:labels] if params[:labels].present?
53 54 @issue = user_project.issues.new attrs
... ...
lib/api/merge_requests.rb
... ... @@ -4,6 +4,16 @@ module Gitlab
4 4 before { authenticate! }
5 5  
6 6 resource :projects do
  7 + helpers do
  8 + def handle_merge_request_errors!(errors)
  9 + if errors[:project_access].any?
  10 + error!(errors[:project_access], 422)
  11 + elsif errors[:branch_conflict].any?
  12 + error!(errors[:branch_conflict], 422)
  13 + end
  14 + not_found!
  15 + end
  16 + end
7 17  
8 18 # List merge requests
9 19 #
... ... @@ -51,6 +61,7 @@ module Gitlab
51 61 #
52 62 post ":id/merge_requests" do
53 63 authorize! :write_merge_request, user_project
  64 + required_attributes! [:source_branch, :target_branch, :title]
54 65  
55 66 attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title]
56 67 merge_request = user_project.merge_requests.new(attrs)
... ... @@ -60,7 +71,7 @@ module Gitlab
60 71 merge_request.reload_code
61 72 present merge_request, with: Entities::MergeRequest
62 73 else
63   - not_found!
  74 + handle_merge_request_errors! merge_request.errors
64 75 end
65 76 end
66 77  
... ... @@ -88,7 +99,7 @@ module Gitlab
88 99 merge_request.mark_as_unchecked
89 100 present merge_request, with: Entities::MergeRequest
90 101 else
91   - not_found!
  102 + handle_merge_request_errors! merge_request.errors
92 103 end
93 104 end
94 105  
... ... @@ -102,6 +113,8 @@ module Gitlab
102 113 # POST /projects/:id/merge_request/:merge_request_id/comments
103 114 #
104 115 post ":id/merge_request/:merge_request_id/comments" do
  116 + required_attributes! [:note]
  117 +
105 118 merge_request = user_project.merge_requests.find(params[:merge_request_id])
106 119 note = merge_request.notes.new(note: params[:note], project_id: user_project.id)
107 120 note.author = current_user
... ...
lib/api/milestones.rb
... ... @@ -41,6 +41,7 @@ module Gitlab
41 41 # POST /projects/:id/milestones
42 42 post ":id/milestones" do
43 43 authorize! :admin_milestone, user_project
  44 + required_attributes! [:title]
44 45  
45 46 attrs = attributes_for_keys [:title, :description, :due_date]
46 47 @milestone = user_project.milestones.new attrs
... ...
lib/api/notes.rb
... ... @@ -37,12 +37,16 @@ module Gitlab
37 37 # Example Request:
38 38 # POST /projects/:id/notes
39 39 post ":id/notes" do
  40 + required_attributes! [:body]
  41 +
40 42 @note = user_project.notes.new(note: params[:body])
41 43 @note.author = current_user
42 44  
43 45 if @note.save
44 46 present @note, with: Entities::Note
45 47 else
  48 + # :note is exposed as :body, but :note is set on error
  49 + bad_request!(:note) if @note.errors[:note].any?
46 50 not_found!
47 51 end
48 52 end
... ... @@ -89,6 +93,8 @@ module Gitlab
89 93 # POST /projects/:id/issues/:noteable_id/notes
90 94 # POST /projects/:id/snippets/:noteable_id/notes
91 95 post ":id/#{noteables_str}/:#{noteable_id_str}/notes" do
  96 + required_attributes! [:body]
  97 +
92 98 @noteable = user_project.send(:"#{noteables_str}").find(params[:"#{noteable_id_str}"])
93 99 @note = @noteable.notes.new(note: params[:body])
94 100 @note.author = current_user
... ...
lib/api/projects.rb
... ... @@ -4,6 +4,15 @@ module Gitlab
4 4 before { authenticate! }
5 5  
6 6 resource :projects do
  7 + helpers do
  8 + def handle_project_member_errors(errors)
  9 + if errors[:project_access].any?
  10 + error!(errors[:project_access], 422)
  11 + end
  12 + not_found!
  13 + end
  14 + end
  15 +
7 16 # Get a projects list for authenticated user
8 17 #
9 18 # Example Request:
... ... @@ -33,9 +42,11 @@ module Gitlab
33 42 # wall_enabled (optional) - enabled by default
34 43 # merge_requests_enabled (optional) - enabled by default
35 44 # wiki_enabled (optional) - enabled by default
  45 + # namespace_id (optional) - defaults to user namespace
36 46 # Example Request
37 47 # POST /projects
38 48 post do
  49 + required_attributes! [:name]
39 50 attrs = attributes_for_keys [:name,
40 51 :description,
41 52 :default_branch,
... ... @@ -48,6 +59,9 @@ module Gitlab
48 59 if @project.saved?
49 60 present @project, with: Entities::Project
50 61 else
  62 + if @project.errors[:limit_reached].present?
  63 + error!(@project.errors[:limit_reached], 403)
  64 + end
51 65 not_found!
52 66 end
53 67 end
... ... @@ -122,16 +136,22 @@ module Gitlab
122 136 # POST /projects/:id/members
123 137 post ":id/members" do
124 138 authorize! :admin_project, user_project
125   - users_project = user_project.users_projects.new(
126   - user_id: params[:user_id],
127   - project_access: params[:access_level]
128   - )
  139 + required_attributes! [:user_id, :access_level]
  140 +
  141 + # either the user is already a team member or a new one
  142 + team_member = user_project.team_member_by_id(params[:user_id])
  143 + if team_member.nil?
  144 + team_member = user_project.users_projects.new(
  145 + user_id: params[:user_id],
  146 + project_access: params[:access_level]
  147 + )
  148 + end
129 149  
130   - if users_project.save
131   - @member = users_project.user
  150 + if team_member.save
  151 + @member = team_member.user
132 152 present @member, with: Entities::ProjectMember, project: user_project
133 153 else
134   - not_found!
  154 + handle_project_member_errors team_member.errors
135 155 end
136 156 end
137 157  
... ... @@ -145,13 +165,16 @@ module Gitlab
145 165 # PUT /projects/:id/members/:user_id
146 166 put ":id/members/:user_id" do
147 167 authorize! :admin_project, user_project
148   - users_project = user_project.users_projects.find_by_user_id params[:user_id]
  168 + required_attributes! [:access_level]
  169 +
  170 + team_member = user_project.users_projects.find_by_user_id(params[:user_id])
  171 + not_found!("User can not be found") if team_member.nil?
149 172  
150   - if users_project.update_attributes(project_access: params[:access_level])
151   - @member = users_project.user
  173 + if team_member.update_attributes(project_access: params[:access_level])
  174 + @member = team_member.user
152 175 present @member, with: Entities::ProjectMember, project: user_project
153 176 else
154   - not_found!
  177 + handle_project_member_errors team_member.errors
155 178 end
156 179 end
157 180  
... ... @@ -164,8 +187,12 @@ module Gitlab
164 187 # DELETE /projects/:id/members/:user_id
165 188 delete ":id/members/:user_id" do
166 189 authorize! :admin_project, user_project
167   - users_project = user_project.users_projects.find_by_user_id params[:user_id]
168   - users_project.destroy
  190 + team_member = user_project.users_projects.find_by_user_id(params[:user_id])
  191 + unless team_member.nil?
  192 + team_member.destroy
  193 + else
  194 + {:message => "Access revoked", :id => params[:user_id].to_i}
  195 + end
169 196 end
170 197  
171 198 # Get project hooks
... ... @@ -203,11 +230,16 @@ module Gitlab
203 230 # POST /projects/:id/hooks
204 231 post ":id/hooks" do
205 232 authorize! :admin_project, user_project
  233 + required_attributes! [:url]
  234 +
206 235 @hook = user_project.hooks.new({"url" => params[:url]})
207 236 if @hook.save
208 237 present @hook, with: Entities::Hook
209 238 else
210   - error!({'message' => '404 Not found'}, 404)
  239 + if @hook.errors[:url].present?
  240 + error!("Invalid url given", 422)
  241 + end
  242 + not_found!
211 243 end
212 244 end
213 245  
... ... @@ -222,27 +254,36 @@ module Gitlab
222 254 put ":id/hooks/:hook_id" do
223 255 @hook = user_project.hooks.find(params[:hook_id])
224 256 authorize! :admin_project, user_project
  257 + required_attributes! [:url]
225 258  
226 259 attrs = attributes_for_keys [:url]
227   -
228 260 if @hook.update_attributes attrs
229 261 present @hook, with: Entities::Hook
230 262 else
  263 + if @hook.errors[:url].present?
  264 + error!("Invalid url given", 422)
  265 + end
231 266 not_found!
232 267 end
233 268 end
234 269  
235   - # Delete project hook
  270 + # Deletes project hook. This is an idempotent function.
236 271 #
237 272 # Parameters:
238 273 # id (required) - The ID of a project
239 274 # hook_id (required) - The ID of hook to delete
240 275 # Example Request:
241 276 # DELETE /projects/:id/hooks/:hook_id
242   - delete ":id/hooks/:hook_id" do
  277 + delete ":id/hooks" do
243 278 authorize! :admin_project, user_project
244   - @hook = user_project.hooks.find(params[:hook_id])
245   - @hook.destroy
  279 + required_attributes! [:hook_id]
  280 +
  281 + begin
  282 + @hook = ProjectHook.find(params[:hook_id])
  283 + @hook.destroy
  284 + rescue
  285 + # ProjectHook can raise Error if hook_id not found
  286 + end
246 287 end
247 288  
248 289 # Get a project repository branches
... ... @@ -277,6 +318,7 @@ module Gitlab
277 318 # PUT /projects/:id/repository/branches/:branch/protect
278 319 put ":id/repository/branches/:branch/protect" do
279 320 @branch = user_project.repo.heads.find { |item| item.name == params[:branch] }
  321 + not_found! unless @branch
280 322 protected = user_project.protected_branches.find_by_name(@branch.name)
281 323  
282 324 unless protected
... ... @@ -295,6 +337,7 @@ module Gitlab
295 337 # PUT /projects/:id/repository/branches/:branch/unprotect
296 338 put ":id/repository/branches/:branch/unprotect" do
297 339 @branch = user_project.repo.heads.find { |item| item.name == params[:branch] }
  340 + not_found! unless @branch
298 341 protected = user_project.protected_branches.find_by_name(@branch.name)
299 342  
300 343 if protected
... ... @@ -318,7 +361,7 @@ module Gitlab
318 361 #
319 362 # Parameters:
320 363 # id (required) - The ID of a project
321   - # ref_name (optional) - The name of a repository branch or tag
  364 + # ref_name (optional) - The name of a repository branch or tag, if not given the default branch is used
322 365 # Example Request:
323 366 # GET /projects/:id/repository/commits
324 367 get ":id/repository/commits" do
... ... @@ -366,6 +409,7 @@ module Gitlab
366 409 # POST /projects/:id/snippets
367 410 post ":id/snippets" do
368 411 authorize! :write_snippet, user_project
  412 + required_attributes! [:title, :file_name, :code]
369 413  
370 414 attrs = attributes_for_keys [:title, :file_name]
371 415 attrs[:expires_at] = params[:lifetime] if params[:lifetime].present?
... ... @@ -414,10 +458,12 @@ module Gitlab
414 458 # Example Request:
415 459 # DELETE /projects/:id/snippets/:snippet_id
416 460 delete ":id/snippets/:snippet_id" do
417   - @snippet = user_project.snippets.find(params[:snippet_id])
418   - authorize! :modify_snippet, @snippet
419   -
420   - @snippet.destroy
  461 + begin
  462 + @snippet = user_project.snippets.find(params[:snippet_id])
  463 + authorize! :modify_snippet, user_project
  464 + @snippet.destroy
  465 + rescue
  466 + end
421 467 end
422 468  
423 469 # Get a raw project snippet
... ... @@ -443,6 +489,7 @@ module Gitlab
443 489 # GET /projects/:id/repository/commits/:sha/blob
444 490 get ":id/repository/commits/:sha/blob" do
445 491 authorize! :download_code, user_project
  492 + required_attributes! [:filepath]
446 493  
447 494 ref = params[:sha]
448 495  
... ...
lib/api/users.rb
... ... @@ -41,6 +41,8 @@ module Gitlab
41 41 # POST /users
42 42 post do
43 43 authenticated_as_admin!
  44 + required_attributes! [:email, :password, :name, :username]
  45 +
44 46 attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :extern_uid, :provider, :bio]
45 47 user = User.new attrs, as: :admin
46 48 if user.save
... ... @@ -67,10 +69,12 @@ module Gitlab
67 69 # PUT /users/:id
68 70 put ":id" do
69 71 authenticated_as_admin!
  72 +
70 73 attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :extern_uid, :provider, :bio]
71   - user = User.find_by_id(params[:id])
  74 + user = User.find(params[:id])
  75 + not_found!("User not found") unless user
72 76  
73   - if user && user.update_attributes(attrs)
  77 + if user.update_attributes(attrs)
74 78 present user, with: Entities::User
75 79 else
76 80 not_found!
... ... @@ -147,6 +151,8 @@ module Gitlab
147 151 # Example Request:
148 152 # POST /user/keys
149 153 post "keys" do
  154 + required_attributes! [:title, :key]
  155 +
150 156 attrs = attributes_for_keys [:title, :key]
151 157 key = current_user.keys.new attrs
152 158 if key.save
... ... @@ -156,15 +162,18 @@ module Gitlab
156 162 end
157 163 end
158 164  
159   - # Delete existed ssh key of currently authenticated user
  165 + # Delete existing ssh key of currently authenticated user
160 166 #
161 167 # Parameters:
162 168 # id (required) - SSH Key ID
163 169 # Example Request:
164 170 # DELETE /user/keys/:id
165 171 delete "keys/:id" do
166   - key = current_user.keys.find params[:id]
167   - key.delete
  172 + begin
  173 + key = current_user.keys.find params[:id]
  174 + key.delete
  175 + rescue
  176 + end
168 177 end
169 178 end
170 179 end
... ...
spec/models/project_spec.rb
... ... @@ -65,7 +65,7 @@ describe Project do
65 65 it "should not allow new projects beyond user limits" do
66 66 project.stub(:creator).and_return(double(can_create_project?: false, projects_limit: 1))
67 67 project.should_not be_valid
68   - project.errors[:base].first.should match(/Your own projects limit is 1/)
  68 + project.errors[:limit_reached].first.should match(/Your own projects limit is 1/)
69 69 end
70 70 end
71 71  
... ...
spec/requests/api/groups_spec.rb
... ... @@ -88,6 +88,16 @@ describe Gitlab::API do
88 88 post api("/groups", admin), {:name => "Duplicate Test", :path => group2.path}
89 89 response.status.should == 404
90 90 end
  91 +
  92 + it "should return 400 bad request error if name not given" do
  93 + post api("/groups", admin), { :path => group2.path }
  94 + response.status.should == 400
  95 + end
  96 +
  97 + it "should return 400 bad request error if path not given" do
  98 + post api("/groups", admin), { :name => 'test' }
  99 + response.status.should == 400
  100 + end
91 101 end
92 102 end
93 103  
... ...
spec/requests/api/issues_spec.rb
... ... @@ -41,6 +41,11 @@ describe Gitlab::API do
41 41 response.status.should == 200
42 42 json_response['title'].should == issue.title
43 43 end
  44 +
  45 + it "should return 404 if issue id not found" do
  46 + get api("/projects/#{project.id}/issues/54321", user)
  47 + response.status.should == 404
  48 + end
44 49 end
45 50  
46 51 describe "POST /projects/:id/issues" do
... ... @@ -52,6 +57,11 @@ describe Gitlab::API do
52 57 json_response['description'].should be_nil
53 58 json_response['labels'].should == ['label', 'label2']
54 59 end
  60 +
  61 + it "should return a 400 bad request if title not given" do
  62 + post api("/projects/#{project.id}/issues", user), labels: 'label, label2'
  63 + response.status.should == 400
  64 + end
55 65 end
56 66  
57 67 describe "PUT /projects/:id/issues/:issue_id to update only title" do
... ... @@ -62,6 +72,12 @@ describe Gitlab::API do
62 72  
63 73 json_response['title'].should == 'updated title'
64 74 end
  75 +
  76 + it "should return 404 error if issue id not found" do
  77 + put api("/projects/#{project.id}/issues/44444", user),
  78 + title: 'updated title'
  79 + response.status.should == 404
  80 + end
65 81 end
66 82  
67 83 describe "PUT /projects/:id/issues/:issue_id to update state and label" do
... ...
spec/requests/api/merge_requests_spec.rb
... ... @@ -32,6 +32,11 @@ describe Gitlab::API do
32 32 response.status.should == 200
33 33 json_response['title'].should == merge_request.title
34 34 end
  35 +
  36 + it "should return a 404 error if merge_request_id not found" do
  37 + get api("/projects/#{project.id}/merge_request/999", user)
  38 + response.status.should == 404
  39 + end
35 40 end
36 41  
37 42 describe "POST /projects/:id/merge_requests" do
... ... @@ -41,6 +46,30 @@ describe Gitlab::API do
41 46 response.status.should == 201
42 47 json_response['title'].should == 'Test merge_request'
43 48 end
  49 +
  50 + it "should return 422 when source_branch equals target_branch" do
  51 + post api("/projects/#{project.id}/merge_requests", user),
  52 + title: "Test merge_request", source_branch: "master", target_branch: "master", author: user
  53 + response.status.should == 422
  54 + end
  55 +
  56 + it "should return 400 when source_branch is missing" do
  57 + post api("/projects/#{project.id}/merge_requests", user),
  58 + title: "Test merge_request", target_branch: "master", author: user
  59 + response.status.should == 400
  60 + end
  61 +
  62 + it "should return 400 when target_branch is missing" do
  63 + post api("/projects/#{project.id}/merge_requests", user),
  64 + title: "Test merge_request", source_branch: "stable", author: user
  65 + response.status.should == 400
  66 + end
  67 +
  68 + it "should return 400 when title is missing" do
  69 + post api("/projects/#{project.id}/merge_requests", user),
  70 + target_branch: 'master', source_branch: 'stable'
  71 + response.status.should == 400
  72 + end
44 73 end
45 74  
46 75 describe "PUT /projects/:id/merge_request/:merge_request_id to close MR" do
... ... @@ -59,13 +88,24 @@ describe Gitlab::API do
59 88 end
60 89 end
61 90  
62   -
63 91 describe "PUT /projects/:id/merge_request/:merge_request_id" do
64 92 it "should return merge_request" do
65 93 put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), title: "New title"
66 94 response.status.should == 200
67 95 json_response['title'].should == 'New title'
68 96 end
  97 +
  98 + it "should return 422 when source_branch and target_branch are renamed the same" do
  99 + put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user),
  100 + source_branch: "master", target_branch: "master"
  101 + response.status.should == 422
  102 + end
  103 +
  104 + it "should return merge_request with renamed target_branch" do
  105 + put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), target_branch: "test"
  106 + response.status.should == 200
  107 + json_response['target_branch'].should == 'test'
  108 + end
69 109 end
70 110  
71 111 describe "POST /projects/:id/merge_request/:merge_request_id/comments" do
... ... @@ -74,6 +114,16 @@ describe Gitlab::API do
74 114 response.status.should == 201
75 115 json_response['note'].should == 'My comment'
76 116 end
  117 +
  118 + it "should return 400 if note is missing" do
  119 + post api("/projects/#{project.id}/merge_request/#{merge_request.id}/comments", user)
  120 + response.status.should == 400
  121 + end
  122 +
  123 + it "should return 404 if note is attached to non existent merge request" do
  124 + post api("/projects/#{project.id}/merge_request/111/comments", user), note: "My comment"
  125 + response.status.should == 404
  126 + end
77 127 end
78 128  
79 129 end
... ...
spec/requests/api/milestones_spec.rb
... ... @@ -16,6 +16,11 @@ describe Gitlab::API do
16 16 json_response.should be_an Array
17 17 json_response.first['title'].should == milestone.title
18 18 end
  19 +
  20 + it "should return a 401 error if user not authenticated" do
  21 + get api("/projects/#{project.id}/milestones")
  22 + response.status.should == 401
  23 + end
19 24 end
20 25  
21 26 describe "GET /projects/:id/milestones/:milestone_id" do
... ... @@ -24,16 +29,38 @@ describe Gitlab::API do
24 29 response.status.should == 200
25 30 json_response['title'].should == milestone.title
26 31 end
  32 +
  33 + it "should return 401 error if user not authenticated" do
  34 + get api("/projects/#{project.id}/milestones/#{milestone.id}")
  35 + response.status.should == 401
  36 + end
  37 +
  38 + it "should return a 404 error if milestone id not found" do
  39 + get api("/projects/#{project.id}/milestones/1234", user)
  40 + response.status.should == 404
  41 + end
27 42 end
28 43  
29 44 describe "POST /projects/:id/milestones" do
30 45 it "should create a new project milestone" do
31   - post api("/projects/#{project.id}/milestones", user),
32   - title: 'new milestone'
  46 + post api("/projects/#{project.id}/milestones", user), title: 'new milestone'
33 47 response.status.should == 201
34 48 json_response['title'].should == 'new milestone'
35 49 json_response['description'].should be_nil
36 50 end
  51 +
  52 + it "should create a new project milestone with description and due date" do
  53 + post api("/projects/#{project.id}/milestones", user),
  54 + title: 'new milestone', description: 'release', due_date: '2013-03-02'
  55 + response.status.should == 201
  56 + json_response['description'].should == 'release'
  57 + json_response['due_date'].should == '2013-03-02'
  58 + end
  59 +
  60 + it "should return a 400 error if title is missing" do
  61 + post api("/projects/#{project.id}/milestones", user)
  62 + response.status.should == 400
  63 + end
37 64 end
38 65  
39 66 describe "PUT /projects/:id/milestones/:milestone_id" do
... ... @@ -43,6 +70,12 @@ describe Gitlab::API do
43 70 response.status.should == 200
44 71 json_response['title'].should == 'updated title'
45 72 end
  73 +
  74 + it "should return a 404 error if milestone id not found" do
  75 + put api("/projects/#{project.id}/milestones/1234", user),
  76 + title: 'updated title'
  77 + response.status.should == 404
  78 + end
46 79 end
47 80  
48 81 describe "PUT /projects/:id/milestones/:milestone_id to close milestone" do
... ...
spec/requests/api/notes_spec.rb
... ... @@ -38,6 +38,11 @@ describe Gitlab::API do
38 38 response.status.should == 200
39 39 json_response['body'].should == wall_note.note
40 40 end
  41 +
  42 + it "should return a 404 error if note not found" do
  43 + get api("/projects/#{project.id}/notes/123", user)
  44 + response.status.should == 404
  45 + end
41 46 end
42 47  
43 48 describe "POST /projects/:id/notes" do
... ... @@ -46,6 +51,16 @@ describe Gitlab::API do
46 51 response.status.should == 201
47 52 json_response['body'].should == 'hi!'
48 53 end
  54 +
  55 + it "should return 401 unauthorized error" do
  56 + post api("/projects/#{project.id}/notes")
  57 + response.status.should == 401
  58 + end
  59 +
  60 + it "should return a 400 bad request if body is missing" do
  61 + post api("/projects/#{project.id}/notes", user)
  62 + response.status.should == 400
  63 + end
49 64 end
50 65  
51 66 describe "GET /projects/:id/noteable/:noteable_id/notes" do
... ... @@ -56,6 +71,11 @@ describe Gitlab::API do
56 71 json_response.should be_an Array
57 72 json_response.first['body'].should == issue_note.note
58 73 end
  74 +
  75 + it "should return a 404 error when issue id not found" do
  76 + get api("/projects/#{project.id}/issues/123/notes", user)
  77 + response.status.should == 404
  78 + end
59 79 end
60 80  
61 81 context "when noteable is a Snippet" do
... ... @@ -65,6 +85,11 @@ describe Gitlab::API do
65 85 json_response.should be_an Array
66 86 json_response.first['body'].should == snippet_note.note
67 87 end
  88 +
  89 + it "should return a 404 error when snippet id not found" do
  90 + get api("/projects/#{project.id}/snippets/42/notes", user)
  91 + response.status.should == 404
  92 + end
68 93 end
69 94  
70 95 context "when noteable is a Merge Request" do
... ... @@ -74,6 +99,11 @@ describe Gitlab::API do
74 99 json_response.should be_an Array
75 100 json_response.first['body'].should == merge_request_note.note
76 101 end
  102 +
  103 + it "should return a 404 error if merge request id not found" do
  104 + get api("/projects/#{project.id}/merge_requests/4444/notes", user)
  105 + response.status.should == 404
  106 + end
77 107 end
78 108 end
79 109  
... ... @@ -84,6 +114,11 @@ describe Gitlab::API do
84 114 response.status.should == 200
85 115 json_response['body'].should == issue_note.note
86 116 end
  117 +
  118 + it "should return a 404 error if issue note not found" do
  119 + get api("/projects/#{project.id}/issues/#{issue.id}/notes/123", user)
  120 + response.status.should == 404
  121 + end
87 122 end
88 123  
89 124 context "when noteable is a Snippet" do
... ... @@ -92,6 +127,11 @@ describe Gitlab::API do
92 127 response.status.should == 200
93 128 json_response['body'].should == snippet_note.note
94 129 end
  130 +
  131 + it "should return a 404 error if snippet note not found" do
  132 + get api("/projects/#{project.id}/snippets/#{snippet.id}/notes/123", user)
  133 + response.status.should == 404
  134 + end
95 135 end
96 136 end
97 137  
... ... @@ -103,6 +143,16 @@ describe Gitlab::API do
103 143 json_response['body'].should == 'hi!'
104 144 json_response['author']['email'].should == user.email
105 145 end
  146 +
  147 + it "should return a 400 bad request error if body not given" do
  148 + post api("/projects/#{project.id}/issues/#{issue.id}/notes", user)
  149 + response.status.should == 400
  150 + end
  151 +
  152 + it "should return a 401 unauthorized error if user not authenticated" do
  153 + post api("/projects/#{project.id}/issues/#{issue.id}/notes"), body: 'hi!'
  154 + response.status.should == 401
  155 + end
106 156 end
107 157  
108 158 context "when noteable is a Snippet" do
... ... @@ -112,6 +162,16 @@ describe Gitlab::API do
112 162 json_response['body'].should == 'hi!'
113 163 json_response['author']['email'].should == user.email
114 164 end
  165 +
  166 + it "should return a 400 bad request error if body not given" do
  167 + post api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user)
  168 + response.status.should == 400
  169 + end
  170 +
  171 + it "should return a 401 unauthorized error if user not authenticated" do
  172 + post api("/projects/#{project.id}/snippets/#{snippet.id}/notes"), body: 'hi!'
  173 + response.status.should == 401
  174 + end
115 175 end
116 176 end
117 177 end
... ...
spec/requests/api/projects_spec.rb
... ... @@ -7,8 +7,8 @@ describe Gitlab::API do
7 7 let(:user2) { create(:user) }
8 8 let(:user3) { create(:user) }
9 9 let(:admin) { create(:admin) }
10   - let!(:hook) { create(:project_hook, project: project, url: "http://example.com") }
11 10 let!(:project) { create(:project, namespace: user.namespace ) }
  11 + let!(:hook) { create(:project_hook, project: project, url: "http://example.com") }
12 12 let!(:snippet) { create(:snippet, author: user, project: project, title: 'example') }
13 13 let!(:users_project) { create(:users_project, user: user, project: project, project_access: UsersProject::MASTER) }
14 14 let!(:users_project2) { create(:users_project, user: user3, project: project, project_access: UsersProject::DEVELOPER) }
... ... @@ -58,6 +58,11 @@ describe Gitlab::API do
58 58 expect { post api("/projects", user) }.to_not change {Project.count}
59 59 end
60 60  
  61 + it "should return a 400 error if name not given" do
  62 + post api("/projects", user)
  63 + response.status.should == 400
  64 + end
  65 +
61 66 it "should create last project before reaching project limit" do
62 67 (1..user2.projects_limit-1).each { |p| post api("/projects", user2), name: "foo#{p}" }
63 68 post api("/projects", user2), name: "foo"
... ... @@ -69,9 +74,17 @@ describe Gitlab::API do
69 74 response.status.should == 201
70 75 end
71 76  
72   - it "should respond with 404 on failure" do
  77 + it "should respond with 400 if name is not given" do
73 78 post api("/projects", user)
74   - response.status.should == 404
  79 + response.status.should == 400
  80 + end
  81 +
  82 + it "should return a 403 error if project limit reached" do
  83 + (1..user.projects_limit).each do |p|
  84 + post api("/projects", user), name: "foo#{p}"
  85 + end
  86 + post api("/projects", user), name: 'bar'
  87 + response.status.should == 403
75 88 end
76 89  
77 90 it "should assign attributes to project" do
... ... @@ -152,6 +165,12 @@ describe Gitlab::API do
152 165 response.status.should == 404
153 166 json_response['message'].should == '404 Not Found'
154 167 end
  168 +
  169 + it "should return a 404 error if user is not a member" do
  170 + other_user = create(:user)
  171 + get api("/projects/#{project.id}", other_user)
  172 + response.status.should == 404
  173 + end
155 174 end
156 175  
157 176 describe "GET /projects/:id/repository/branches" do
... ... @@ -188,6 +207,17 @@ describe Gitlab::API do
188 207 json_response['commit']['id'].should == '621491c677087aa243f165eab467bfdfbee00be1'
189 208 json_response['protected'].should == true
190 209 end
  210 +
  211 + it "should return a 404 error if branch not found" do
  212 + put api("/projects/#{project.id}/repository/branches/unknown/protect", user)
  213 + response.status.should == 404
  214 + end
  215 +
  216 + it "should return success when protect branch again" do
  217 + put api("/projects/#{project.id}/repository/branches/new_design/protect", user)
  218 + put api("/projects/#{project.id}/repository/branches/new_design/protect", user)
  219 + response.status.should == 200
  220 + end
191 221 end
192 222  
193 223 describe "PUT /projects/:id/repository/branches/:branch/unprotect" do
... ... @@ -199,6 +229,17 @@ describe Gitlab::API do
199 229 json_response['commit']['id'].should == '621491c677087aa243f165eab467bfdfbee00be1'
200 230 json_response['protected'].should == false
201 231 end
  232 +
  233 + it "should return success when unprotect branch" do
  234 + put api("/projects/#{project.id}/repository/branches/unknown/unprotect", user)
  235 + response.status.should == 404
  236 + end
  237 +
  238 + it "should return success when unprotect branch again" do
  239 + put api("/projects/#{project.id}/repository/branches/new_design/unprotect", user)
  240 + put api("/projects/#{project.id}/repository/branches/new_design/unprotect", user)
  241 + response.status.should == 200
  242 + end
202 243 end
203 244  
204 245 describe "GET /projects/:id/members" do
... ... @@ -217,6 +258,11 @@ describe Gitlab::API do
217 258 json_response.count.should == 1
218 259 json_response.first['email'].should == user.email
219 260 end
  261 +
  262 + it "should return a 404 error if id not found" do
  263 + get api("/projects/9999/members", user)
  264 + response.status.should == 404
  265 + end
220 266 end
221 267  
222 268 describe "GET /projects/:id/members/:user_id" do
... ... @@ -226,6 +272,11 @@ describe Gitlab::API do
226 272 json_response['email'].should == user.email
227 273 json_response['access_level'].should == UsersProject::MASTER
228 274 end
  275 +
  276 + it "should return a 404 error if user id not found" do
  277 + get api("/projects/#{project.id}/members/1234", user)
  278 + response.status.should == 404
  279 + end
229 280 end
230 281  
231 282 describe "POST /projects/:id/members" do
... ... @@ -239,6 +290,34 @@ describe Gitlab::API do
239 290 json_response['email'].should == user2.email
240 291 json_response['access_level'].should == UsersProject::DEVELOPER
241 292 end
  293 +
  294 + it "should return a 201 status if user is already project member" do
  295 + post api("/projects/#{project.id}/members", user), user_id: user2.id,
  296 + access_level: UsersProject::DEVELOPER
  297 + expect {
  298 + post api("/projects/#{project.id}/members", user), user_id: user2.id,
  299 + access_level: UsersProject::DEVELOPER
  300 + }.not_to change { UsersProject.count }.by(1)
  301 +
  302 + response.status.should == 201
  303 + json_response['email'].should == user2.email
  304 + json_response['access_level'].should == UsersProject::DEVELOPER
  305 + end
  306 +
  307 + it "should return a 400 error when user id is not given" do
  308 + post api("/projects/#{project.id}/members", user), access_level: UsersProject::MASTER
  309 + response.status.should == 400
  310 + end
  311 +
  312 + it "should return a 400 error when access level is not given" do
  313 + post api("/projects/#{project.id}/members", user), user_id: user2.id
  314 + response.status.should == 400
  315 + end
  316 +
  317 + it "should return a 422 error when access level is not known" do
  318 + post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: 1234
  319 + response.status.should == 422
  320 + end
242 321 end
243 322  
244 323 describe "PUT /projects/:id/members/:user_id" do
... ... @@ -248,6 +327,21 @@ describe Gitlab::API do
248 327 json_response['email'].should == user3.email
249 328 json_response['access_level'].should == UsersProject::MASTER
250 329 end
  330 +
  331 + it "should return a 404 error if user_id is not found" do
  332 + put api("/projects/#{project.id}/members/1234", user), access_level: UsersProject::MASTER
  333 + response.status.should == 404
  334 + end
  335 +
  336 + it "should return a 400 error when access level is not given" do
  337 + put api("/projects/#{project.id}/members/#{user3.id}", user)
  338 + response.status.should == 400
  339 + end
  340 +
  341 + it "should return a 422 error when access level is not known" do
  342 + put api("/projects/#{project.id}/members/#{user3.id}", user), access_level: 123
  343 + response.status.should == 422
  344 + end
251 345 end
252 346  
253 347 describe "DELETE /projects/:id/members/:user_id" do
... ... @@ -256,6 +350,30 @@ describe Gitlab::API do
256 350 delete api("/projects/#{project.id}/members/#{user3.id}", user)
257 351 }.to change { UsersProject.count }.by(-1)
258 352 end
  353 +
  354 + it "should return 200 if team member is not part of a project" do
  355 + delete api("/projects/#{project.id}/members/#{user3.id}", user)
  356 + expect {
  357 + delete api("/projects/#{project.id}/members/#{user3.id}", user)
  358 + }.to_not change { UsersProject.count }.by(1)
  359 + end
  360 +
  361 + it "should return 200 if team member already removed" do
  362 + delete api("/projects/#{project.id}/members/#{user3.id}", user)
  363 + delete api("/projects/#{project.id}/members/#{user3.id}", user)
  364 + response.status.should == 200
  365 + end
  366 + end
  367 +
  368 + describe "DELETE /projects/:id/members/:user_id" do
  369 + it "should return 200 OK when the user was not member" do
  370 + expect {
  371 + delete api("/projects/#{project.id}/members/1000000", user)
  372 + }.to change { UsersProject.count }.by(0)
  373 + response.status.should == 200
  374 + json_response['message'].should == "Access revoked"
  375 + json_response['id'].should == 1000000
  376 + end
259 377 end
260 378  
261 379 describe "GET /projects/:id/hooks" do
... ... @@ -298,6 +416,11 @@ describe Gitlab::API do
298 416 response.status.should == 403
299 417 end
300 418 end
  419 +
  420 + it "should return a 404 error if hook id is not available" do
  421 + get api("/projects/#{project.id}/hooks/1234", user)
  422 + response.status.should == 404
  423 + end
301 424 end
302 425  
303 426 describe "POST /projects/:id/hooks" do
... ... @@ -306,6 +429,17 @@ describe Gitlab::API do
306 429 post api("/projects/#{project.id}/hooks", user),
307 430 url: "http://example.com"
308 431 }.to change {project.hooks.count}.by(1)
  432 + response.status.should == 201
  433 + end
  434 +
  435 + it "should return a 400 error if url not given" do
  436 + post api("/projects/#{project.id}/hooks", user)
  437 + response.status.should == 400
  438 + end
  439 +
  440 + it "should return a 422 error if url not valid" do
  441 + post api("/projects/#{project.id}/hooks", user), "url" => "ftp://example.com"
  442 + response.status.should == 422
309 443 end
310 444 end
311 445  
... ... @@ -316,13 +450,44 @@ describe Gitlab::API do
316 450 response.status.should == 200
317 451 json_response['url'].should == 'http://example.org'
318 452 end
  453 +
  454 + it "should return 404 error if hook id not found" do
  455 + put api("/projects/#{project.id}/hooks/1234", user), url: 'http://example.org'
  456 + response.status.should == 404
  457 + end
  458 +
  459 + it "should return 400 error if url is not given" do
  460 + put api("/projects/#{project.id}/hooks/#{hook.id}", user)
  461 + response.status.should == 400
  462 + end
  463 +
  464 + it "should return a 422 error if url is not valid" do
  465 + put api("/projects/#{project.id}/hooks/#{hook.id}", user), url: 'ftp://example.com'
  466 + response.status.should == 422
  467 + end
319 468 end
320 469  
321   - describe "DELETE /projects/:id/hooks/:hook_id" do
  470 + describe "DELETE /projects/:id/hooks" do
322 471 it "should delete hook from project" do
323 472 expect {
324   - delete api("/projects/#{project.id}/hooks/#{hook.id}", user)
  473 + delete api("/projects/#{project.id}/hooks", user), hook_id: hook.id
325 474 }.to change {project.hooks.count}.by(-1)
  475 + response.status.should == 200
  476 + end
  477 +
  478 + it "should return success when deleting hook" do
  479 + delete api("/projects/#{project.id}/hooks", user), hook_id: hook.id
  480 + response.status.should == 200
  481 + end
  482 +
  483 + it "should return success when deleting non existent hook" do
  484 + delete api("/projects/#{project.id}/hooks", user), hook_id: 42
  485 + response.status.should == 200
  486 + end
  487 +
  488 + it "should return a 400 error if hook id not given" do
  489 + delete api("/projects/#{project.id}/hooks", user)
  490 + response.status.should == 400
326 491 end
327 492 end
328 493  
... ... @@ -371,6 +536,11 @@ describe Gitlab::API do
371 536 response.status.should == 200
372 537 json_response['title'].should == snippet.title
373 538 end
  539 +
  540 + it "should return a 404 error if snippet id not found" do
  541 + get api("/projects/#{project.id}/snippets/1234", user)
  542 + response.status.should == 404
  543 + end
374 544 end
375 545  
376 546 describe "POST /projects/:id/snippets" do
... ... @@ -380,6 +550,24 @@ describe Gitlab::API do
380 550 response.status.should == 201
381 551 json_response['title'].should == 'api test'
382 552 end
  553 +
  554 + it "should return a 400 error if title is not given" do
  555 + post api("/projects/#{project.id}/snippets", user),
  556 + file_name: 'sample.rb', code: 'test'
  557 + response.status.should == 400
  558 + end
  559 +
  560 + it "should return a 400 error if file_name not given" do
  561 + post api("/projects/#{project.id}/snippets", user),
  562 + title: 'api test', code: 'test'
  563 + response.status.should == 400
  564 + end
  565 +
  566 + it "should return a 400 error if code not given" do
  567 + post api("/projects/#{project.id}/snippets", user),
  568 + title: 'api test', file_name: 'sample.rb'
  569 + response.status.should == 400
  570 + end
383 571 end
384 572  
385 573 describe "PUT /projects/:id/snippets/:shippet_id" do
... ... @@ -390,6 +578,13 @@ describe Gitlab::API do
390 578 json_response['title'].should == 'example'
391 579 snippet.reload.content.should == 'updated code'
392 580 end
  581 +
  582 + it "should update an existing project snippet with new title" do
  583 + put api("/projects/#{project.id}/snippets/#{snippet.id}", user),
  584 + title: 'other api test'
  585 + response.status.should == 200
  586 + json_response['title'].should == 'other api test'
  587 + end
393 588 end
394 589  
395 590 describe "DELETE /projects/:id/snippets/:snippet_id" do
... ... @@ -397,6 +592,12 @@ describe Gitlab::API do
397 592 expect {
398 593 delete api("/projects/#{project.id}/snippets/#{snippet.id}", user)
399 594 }.to change { Snippet.count }.by(-1)
  595 + response.status.should == 200
  596 + end
  597 +
  598 + it "should return success when deleting unknown snippet id" do
  599 + delete api("/projects/#{project.id}/snippets/1234", user)
  600 + response.status.should == 200
400 601 end
401 602 end
402 603  
... ... @@ -405,9 +606,14 @@ describe Gitlab::API do
405 606 get api("/projects/#{project.id}/snippets/#{snippet.id}/raw", user)
406 607 response.status.should == 200
407 608 end
  609 +
  610 + it "should return a 404 error if raw project snippet not found" do
  611 + get api("/projects/#{project.id}/snippets/5555/raw", user)
  612 + response.status.should == 404
  613 + end
408 614 end
409 615  
410   - describe "GET /projects/:id/:sha/blob" do
  616 + describe "GET /projects/:id/repository/commits/:sha/blob" do
411 617 it "should get the raw file contents" do
412 618 get api("/projects/#{project.id}/repository/commits/master/blob?filepath=README.md", user)
413 619 response.status.should == 200
... ... @@ -422,6 +628,11 @@ describe Gitlab::API do
422 628 get api("/projects/#{project.id}/repository/commits/master/blob?filepath=README.invalid", user)
423 629 response.status.should == 404
424 630 end
  631 +
  632 + it "should return a 400 error if filepath is missing" do
  633 + get api("/projects/#{project.id}/repository/commits/master/blob", user)
  634 + response.status.should == 400
  635 + end
425 636 end
426 637  
427 638 describe "GET /projects/:id/keys" do
... ...
spec/requests/api/session_spec.rb
... ... @@ -35,5 +35,15 @@ describe Gitlab::API do
35 35 json_response['private_token'].should be_nil
36 36 end
37 37 end
  38 +
  39 + context "when empty name" do
  40 + it "should return authentication error" do
  41 + post api("/session"), password: user.password
  42 + response.status.should == 401
  43 +
  44 + json_response['email'].should be_nil
  45 + json_response['private_token'].should be_nil
  46 + end
  47 + end
38 48 end
39 49 end
... ...
spec/requests/api/users_spec.rb
... ... @@ -31,15 +31,20 @@ describe Gitlab::API do
31 31 response.status.should == 200
32 32 json_response['email'].should == user.email
33 33 end
34   - end
35 34  
36   - describe "POST /users" do
37   - before{ admin }
  35 + it "should return a 401 if unauthenticated" do
  36 + get api("/users/9998")
  37 + response.status.should == 401
  38 + end
38 39  
39   - it "should not create invalid user" do
40   - post api("/users", admin), { email: "invalid email" }
  40 + it "should return a 404 error if user id not found" do
  41 + get api("/users/9999", user)
41 42 response.status.should == 404
42 43 end
  44 + end
  45 +
  46 + describe "POST /users" do
  47 + before{ admin }
43 48  
44 49 it "should create user" do
45 50 expect {
... ... @@ -47,10 +52,48 @@ describe Gitlab::API do
47 52 }.to change { User.count }.by(1)
48 53 end
49 54  
  55 + it "should return 201 Created on success" do
  56 + post api("/users", admin), attributes_for(:user, projects_limit: 3)
  57 + response.status.should == 201
  58 + end
  59 +
  60 + it "should not create user with invalid email" do
  61 + post api("/users", admin), { email: "invalid email", password: 'password' }
  62 + response.status.should == 400
  63 + end
  64 +
  65 + it "should return 400 error if password not given" do
  66 + post api("/users", admin), { email: 'test@example.com' }
  67 + response.status.should == 400
  68 + end
  69 +
  70 + it "should return 400 error if email not given" do
  71 + post api("/users", admin), { password: 'pass1234' }
  72 + response.status.should == 400
  73 + end
  74 +
50 75 it "shouldn't available for non admin users" do
51 76 post api("/users", user), attributes_for(:user)
52 77 response.status.should == 403
53 78 end
  79 +
  80 + context "with existing user" do
  81 + before { post api("/users", admin), { email: 'test@example.com', password: 'password', username: 'test' } }
  82 +
  83 + it "should not create user with same email" do
  84 + expect {
  85 + post api("/users", admin), { email: 'test@example.com', password: 'password' }
  86 + }.to change { User.count }.by(0)
  87 + end
  88 +
  89 + it "should return 409 conflict error if user with email exists" do
  90 + post api("/users", admin), { email: 'test@example.com', password: 'password' }
  91 + end
  92 +
  93 + it "should return 409 conflict error if same username exists" do
  94 + post api("/users", admin), { email: 'foo@example.com', password: 'pass', username: 'test' }
  95 + end
  96 + end
54 97 end
55 98  
56 99 describe "GET /users/sign_up" do
... ... @@ -81,7 +124,7 @@ describe Gitlab::API do
81 124 describe "PUT /users/:id" do
82 125 before { admin }
83 126  
84   - it "should update user" do
  127 + it "should update user with new bio" do
85 128 put api("/users/#{user.id}", admin), {bio: 'new test bio'}
86 129 response.status.should == 200
87 130 json_response['bio'].should == 'new test bio'
... ... @@ -103,6 +146,25 @@ describe Gitlab::API do
103 146 put api("/users/999999", admin), {bio: 'update should fail'}
104 147 response.status.should == 404
105 148 end
  149 +
  150 + context "with existing user" do
  151 + before {
  152 + post api("/users", admin), { email: 'test@example.com', password: 'password', username: 'test', name: 'test' }
  153 + post api("/users", admin), { email: 'foo@bar.com', password: 'password', username: 'john', name: 'john' }
  154 + @user_id = User.all.last.id
  155 + }
  156 +
  157 +# it "should return 409 conflict error if email address exists" do
  158 +# put api("/users/#{@user_id}", admin), { email: 'test@example.com' }
  159 +# response.status.should == 409
  160 +# end
  161 +#
  162 +# it "should return 409 conflict error if username taken" do
  163 +# @user_id = User.all.last.id
  164 +# put api("/users/#{@user_id}", admin), { username: 'test' }
  165 +# response.status.should == 409
  166 +# end
  167 + end
106 168 end
107 169  
108 170 describe "POST /users/:id/keys" do
... ... @@ -131,6 +193,11 @@ describe Gitlab::API do
131 193 json_response['email'].should == user.email
132 194 end
133 195  
  196 + it "should not delete for unauthenticated user" do
  197 + delete api("/users/#{user.id}")
  198 + response.status.should == 401
  199 + end
  200 +
134 201 it "shouldn't available for non admin users" do
135 202 delete api("/users/#{user.id}", user)
136 203 response.status.should == 403
... ... @@ -148,6 +215,11 @@ describe Gitlab::API do
148 215 response.status.should == 200
149 216 json_response['email'].should == user.email
150 217 end
  218 +
  219 + it "should return 401 error if user is unauthenticated" do
  220 + get api("/user")
  221 + response.status.should == 401
  222 + end
151 223 end
152 224  
153 225 describe "GET /user/keys" do
... ... @@ -183,19 +255,38 @@ describe Gitlab::API do
183 255 get api("/user/keys/42", user)
184 256 response.status.should == 404
185 257 end
186   - end
187 258  
188   - describe "POST /user/keys" do
189   - it "should not create invalid ssh key" do
190   - post api("/user/keys", user), { title: "invalid key" }
  259 + it "should return 404 error if admin accesses user's ssh key" do
  260 + user.keys << key
  261 + user.save
  262 + admin
  263 + get api("/user/keys/#{key.id}", admin)
191 264 response.status.should == 404
192 265 end
  266 + end
193 267  
  268 + describe "POST /user/keys" do
194 269 it "should create ssh key" do
195 270 key_attrs = attributes_for :key
196 271 expect {
197 272 post api("/user/keys", user), key_attrs
198 273 }.to change{ user.keys.count }.by(1)
  274 + response.status.should == 201
  275 + end
  276 +
  277 + it "should return a 401 error if unauthorized" do
  278 + post api("/user/keys"), title: 'some title', key: 'some key'
  279 + response.status.should == 401
  280 + end
  281 +
  282 + it "should not create ssh key without key" do
  283 + post api("/user/keys", user), title: 'title'
  284 + response.status.should == 400
  285 + end
  286 +
  287 + it "should not create ssh key without title" do
  288 + post api("/user/keys", user), key: "somekey"
  289 + response.status.should == 400
199 290 end
200 291 end
201 292  
... ... @@ -206,11 +297,19 @@ describe Gitlab::API do
206 297 expect {
207 298 delete api("/user/keys/#{key.id}", user)
208 299 }.to change{user.keys.count}.by(-1)
  300 + response.status.should == 200
209 301 end
210 302  
211   - it "should return 404 Not Found within invalid ID" do
  303 + it "should return sucess if key ID not found" do
212 304 delete api("/user/keys/42", user)
213   - response.status.should == 404
  305 + response.status.should == 200
  306 + end
  307 +
  308 + it "should return 401 error if unauthorized" do
  309 + user.keys << key
  310 + user.save
  311 + delete api("/user/keys/#{key.id}")
  312 + response.status.should == 401
214 313 end
215 314 end
216 315 end
... ...