1# frozen_string_literal: true 2 3require 'spec_helper' 4 5RSpec.describe API::Issues do 6 let_it_be(:user) { create(:user) } 7 let_it_be(:owner) { create(:owner) } 8 let(:user2) { create(:user) } 9 let(:non_member) { create(:user) } 10 let_it_be(:guest) { create(:user) } 11 let_it_be(:author) { create(:author) } 12 let_it_be(:assignee) { create(:assignee) } 13 let(:admin) { create(:user, :admin) } 14 let(:issue_title) { 'foo' } 15 let(:issue_description) { 'closed' } 16 17 let_it_be(:project, reload: true) do 18 create(:project, :public, creator_id: owner.id, namespace: owner.namespace) 19 end 20 21 let!(:closed_issue) do 22 create :closed_issue, 23 author: user, 24 assignees: [user], 25 project: project, 26 state: :closed, 27 milestone: milestone, 28 created_at: generate(:past_time), 29 updated_at: 3.hours.ago, 30 closed_at: 1.hour.ago 31 end 32 33 let!(:confidential_issue) do 34 create :issue, 35 :confidential, 36 project: project, 37 author: author, 38 assignees: [assignee], 39 created_at: generate(:past_time), 40 updated_at: 2.hours.ago 41 end 42 43 let!(:issue) do 44 create :issue, 45 author: user, 46 assignees: [user], 47 project: project, 48 milestone: milestone, 49 created_at: generate(:past_time), 50 updated_at: 1.hour.ago, 51 title: issue_title, 52 description: issue_description 53 end 54 55 let_it_be(:label) do 56 create(:label, title: 'label', color: '#FFAABB', project: project) 57 end 58 59 let!(:label_link) { create(:label_link, label: label, target: issue) } 60 let(:milestone) { create(:milestone, title: '1.0.0', project: project) } 61 62 let_it_be(:empty_milestone) do 63 create(:milestone, title: '2.0.0', project: project) 64 end 65 66 let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) } 67 let(:no_milestone_title) { 'None' } 68 let(:any_milestone_title) { 'Any' } 69 let(:updated_title) { 'updated title' } 70 let(:issue_path) { "/projects/#{project.id}/issues/#{issue.iid}" } 71 let(:api_for_user) { api(issue_path, user) } 72 73 before_all do 74 project.add_reporter(user) 75 project.add_guest(guest) 76 end 77 78 before do 79 stub_licensed_features(multiple_issue_assignees: false, issue_weights: false) 80 end 81 82 describe 'PUT /projects/:id/issues/:issue_iid to update only title' do 83 it 'updates a project issue' do 84 put api_for_user, params: { title: updated_title } 85 86 expect(response).to have_gitlab_http_status(:ok) 87 expect(json_response['title']).to eq(updated_title) 88 end 89 90 it 'returns 404 error if issue iid not found' do 91 put api("/projects/#{project.id}/issues/44444", user), params: { title: updated_title } 92 93 expect(response).to have_gitlab_http_status(:not_found) 94 end 95 96 it 'returns 404 error if issue id is used instead of the iid' do 97 put api("/projects/#{project.id}/issues/#{issue.id}", user), params: { title: updated_title } 98 99 expect(response).to have_gitlab_http_status(:not_found) 100 end 101 102 it 'allows special label names' do 103 put api_for_user, 104 params: { 105 title: updated_title, 106 labels: 'label, label?, label&foo, ?, &' 107 } 108 109 expect(response).to have_gitlab_http_status(:ok) 110 end 111 112 it 'allows special label names with labels param as array' do 113 put api_for_user, 114 params: { 115 title: updated_title, 116 labels: ['label', 'label?', 'label&foo, ?, &'] 117 } 118 119 expect(response).to have_gitlab_http_status(:ok) 120 expect(json_response['labels']).to contain_exactly('label', 'label?', 'label&foo', '?', '&') 121 end 122 123 context 'confidential issues' do 124 let(:confidential_issue_path) { "/projects/#{project.id}/issues/#{confidential_issue.iid}" } 125 126 it 'returns 403 for non project members' do 127 put api(confidential_issue_path, non_member), params: { title: updated_title } 128 129 expect(response).to have_gitlab_http_status(:forbidden) 130 end 131 132 it 'returns 403 for project members with guest role' do 133 put api(confidential_issue_path, guest), params: { title: updated_title } 134 135 expect(response).to have_gitlab_http_status(:forbidden) 136 end 137 138 it 'updates a confidential issue for project members' do 139 put api(confidential_issue_path, user), params: { title: updated_title } 140 141 expect(response).to have_gitlab_http_status(:ok) 142 expect(json_response['title']).to eq(updated_title) 143 end 144 145 it 'updates a confidential issue for author' do 146 put api(confidential_issue_path, author), params: { title: updated_title } 147 148 expect(response).to have_gitlab_http_status(:ok) 149 expect(json_response['title']).to eq(updated_title) 150 end 151 152 it 'updates a confidential issue for admin' do 153 put api(confidential_issue_path, admin), params: { title: updated_title } 154 155 expect(response).to have_gitlab_http_status(:ok) 156 expect(json_response['title']).to eq(updated_title) 157 end 158 159 it 'sets an issue to confidential' do 160 put api_for_user, params: { confidential: true } 161 162 expect(response).to have_gitlab_http_status(:ok) 163 expect(json_response['confidential']).to be_truthy 164 end 165 166 it 'makes a confidential issue public' do 167 put api(confidential_issue_path, user), params: { confidential: false } 168 169 expect(response).to have_gitlab_http_status(:ok) 170 expect(json_response['confidential']).to be_falsy 171 end 172 173 it 'does not update a confidential issue with wrong confidential flag' do 174 put api(confidential_issue_path, user), params: { confidential: 'foo' } 175 176 expect(response).to have_gitlab_http_status(:bad_request) 177 expect(json_response['error']).to eq('confidential is invalid') 178 end 179 end 180 end 181 182 describe 'PUT /projects/:id/issues/:issue_iid with spam filtering' do 183 include_context 'includes Spam constants' 184 185 def update_issue 186 put api_for_user, params: params 187 end 188 189 let(:params) do 190 { 191 title: updated_title, 192 description: 'content here', 193 labels: 'label, label2' 194 } 195 end 196 197 before do 198 expect_next_instance_of(Spam::SpamActionService) do |spam_service| 199 expect(spam_service).to receive_messages(check_for_spam?: true) 200 end 201 202 expect_next_instance_of(Spam::SpamVerdictService) do |verdict_service| 203 expect(verdict_service).to receive(:execute).and_return(DISALLOW) 204 end 205 end 206 207 context 'when allow_possible_spam feature flag is false' do 208 before do 209 stub_feature_flags(allow_possible_spam: false) 210 end 211 212 it 'does not update a project issue' do 213 expect { update_issue }.not_to change { issue.reload.title } 214 end 215 216 it 'returns correct status and message' do 217 update_issue 218 219 expect(response).to have_gitlab_http_status(:bad_request) 220 expect(json_response).to include('message' => { 'error' => 'Spam detected' }) 221 end 222 223 it 'creates a new spam log entry' do 224 expect { update_issue } 225 .to log_spam(title: updated_title, description: 'content here', user_id: user.id, noteable_type: 'Issue') 226 end 227 end 228 229 context 'when allow_possible_spam feature flag is true' do 230 it 'updates a project issue' do 231 expect { update_issue }.to change { issue.reload.title } 232 end 233 234 it 'returns correct status and message' do 235 update_issue 236 237 expect(response).to have_gitlab_http_status(:ok) 238 end 239 240 it 'creates a new spam log entry' do 241 expect { update_issue } 242 .to log_spam(title: updated_title, description: 'content here', user_id: user.id, noteable_type: 'Issue') 243 end 244 end 245 end 246 247 describe 'PUT /projects/:id/issues/:issue_iid to update assignee' do 248 context 'support for deprecated assignee_id' do 249 it 'removes assignee' do 250 put api_for_user, params: { assignee_id: 0 } 251 252 expect(response).to have_gitlab_http_status(:ok) 253 expect(json_response['assignee']).to be_nil 254 end 255 256 it 'updates an issue with new assignee' do 257 put api_for_user, params: { assignee_id: user2.id } 258 259 expect(response).to have_gitlab_http_status(:ok) 260 expect(json_response['assignee']['name']).to eq(user2.name) 261 end 262 end 263 264 it 'removes assignee' do 265 put api_for_user, params: { assignee_ids: [0] } 266 267 expect(response).to have_gitlab_http_status(:ok) 268 expect(json_response['assignees']).to be_empty 269 end 270 271 it 'updates an issue with new assignee' do 272 put api_for_user, params: { assignee_ids: [user2.id] } 273 274 expect(response).to have_gitlab_http_status(:ok) 275 expect(json_response['assignees'].first['name']).to eq(user2.name) 276 end 277 278 context 'single assignee restrictions' do 279 it 'updates an issue with several assignees but only one has been applied' do 280 put api_for_user, params: { assignee_ids: [user2.id, guest.id] } 281 282 expect(response).to have_gitlab_http_status(:ok) 283 expect(json_response['assignees'].size).to eq(1) 284 end 285 end 286 end 287 288 describe 'PUT /projects/:id/issues/:issue_iid to update labels' do 289 let!(:label) { create(:label, title: 'dummy', project: project) } 290 let!(:label_link) { create(:label_link, label: label, target: issue) } 291 292 it 'adds relevant labels' do 293 put api_for_user, params: { add_labels: '1, 2' } 294 295 expect(response).to have_gitlab_http_status(:ok) 296 expect(json_response['labels']).to contain_exactly(label.title, '1', '2') 297 end 298 299 context 'removes' do 300 let!(:label2) { create(:label, title: 'a-label', project: project) } 301 let!(:label_link2) { create(:label_link, label: label2, target: issue) } 302 303 it 'removes relevant labels' do 304 put api_for_user, params: { remove_labels: label2.title } 305 306 expect(response).to have_gitlab_http_status(:ok) 307 expect(json_response['labels']).to eq([label.title]) 308 end 309 310 it 'removes all labels' do 311 put api_for_user, params: { remove_labels: "#{label.title}, #{label2.title}" } 312 313 expect(response).to have_gitlab_http_status(:ok) 314 expect(json_response['labels']).to be_empty 315 end 316 end 317 318 it 'does not update labels if not present' do 319 put api_for_user, params: { title: updated_title } 320 321 expect(response).to have_gitlab_http_status(:ok) 322 expect(json_response['labels']).to eq([label.title]) 323 end 324 325 it 'removes all labels and touches the record' do 326 Timecop.travel(1.minute.from_now) do 327 put api_for_user, params: { labels: '' } 328 end 329 330 expect(response).to have_gitlab_http_status(:ok) 331 expect(json_response['labels']).to eq([]) 332 expect(json_response['updated_at']).to be > Time.now 333 end 334 335 it 'removes all labels and touches the record with labels param as array' do 336 Timecop.travel(1.minute.from_now) do 337 put api_for_user, params: { labels: [''] } 338 end 339 340 expect(response).to have_gitlab_http_status(:ok) 341 expect(json_response['labels']).to eq([]) 342 expect(json_response['updated_at']).to be > Time.now 343 end 344 345 it 'updates labels and touches the record' do 346 Timecop.travel(1.minute.from_now) do 347 put api_for_user, params: { labels: 'foo,bar' } 348 end 349 350 expect(response).to have_gitlab_http_status(:ok) 351 expect(json_response['labels']).to contain_exactly('foo', 'bar') 352 expect(json_response['updated_at']).to be > Time.now 353 end 354 355 it 'updates labels and touches the record with labels param as array' do 356 Timecop.travel(1.minute.from_now) do 357 put api_for_user, params: { labels: %w(foo bar) } 358 end 359 360 expect(response).to have_gitlab_http_status(:ok) 361 expect(json_response['labels']).to include 'foo' 362 expect(json_response['labels']).to include 'bar' 363 expect(json_response['updated_at']).to be > Time.now 364 end 365 366 it 'allows special label names' do 367 put api_for_user, params: { labels: 'label:foo, label-bar,label_bar,label/bar,label?bar,label&bar,?,&' } 368 369 expect(response).to have_gitlab_http_status(:ok) 370 expect(json_response['labels']).to contain_exactly('label:foo', 'label-bar', 'label_bar', 'label/bar', 'label?bar', 'label&bar', '?', '&') 371 end 372 373 it 'allows special label names with labels param as array' do 374 put api_for_user, params: { labels: ['label:foo', 'label-bar', 'label_bar', 'label/bar,label?bar,label&bar,?,&'] } 375 376 expect(response).to have_gitlab_http_status(:ok) 377 expect(json_response['labels']).to contain_exactly('label:foo', 'label-bar', 'label_bar', 'label/bar', 'label?bar', 'label&bar', '?', '&') 378 end 379 380 it 'returns 400 if title is too long' do 381 put api_for_user, params: { title: 'g' * 256 } 382 383 expect(response).to have_gitlab_http_status(:bad_request) 384 expect(json_response['message']['title']).to eq([ 385 'is too long (maximum is 255 characters)' 386 ]) 387 end 388 end 389 390 describe 'PUT /projects/:id/issues/:issue_iid to update state and label' do 391 it 'updates a project issue' do 392 put api_for_user, params: { labels: 'label2', state_event: 'close' } 393 394 expect(response).to have_gitlab_http_status(:ok) 395 expect(json_response['labels']).to contain_exactly('label2') 396 expect(json_response['state']).to eq 'closed' 397 end 398 399 it 'reopens a project isssue' do 400 put api(issue_path, user), params: { state_event: 'reopen' } 401 402 expect(response).to have_gitlab_http_status(:ok) 403 expect(json_response['state']).to eq 'opened' 404 end 405 end 406 407 describe 'PUT /projects/:id/issues/:issue_iid to update updated_at param' do 408 context 'when reporter makes request' do 409 it 'accepts the update date to be set' do 410 update_time = 2.weeks.ago 411 412 put api_for_user, params: { title: 'some new title', updated_at: update_time } 413 414 expect(response).to have_gitlab_http_status(:ok) 415 expect(json_response['title']).to eq('some new title') 416 expect(Time.parse(json_response['updated_at'])).not_to be_like_time(update_time) 417 end 418 end 419 420 context 'when admin or owner makes the request' do 421 let(:api_for_owner) { api(issue_path, owner) } 422 423 it 'not allow to set null for updated_at' do 424 put api_for_owner, params: { updated_at: nil } 425 426 expect(response).to have_gitlab_http_status(:bad_request) 427 end 428 429 it 'not allow to set blank for updated_at' do 430 put api_for_owner, params: { updated_at: '' } 431 432 expect(response).to have_gitlab_http_status(:bad_request) 433 end 434 435 it 'not allow to set invalid format for updated_at' do 436 put api_for_owner, params: { updated_at: 'invalid-format' } 437 438 expect(response).to have_gitlab_http_status(:bad_request) 439 end 440 441 it 'accepts the update date to be set' do 442 update_time = 2.weeks.ago 443 put api_for_owner, params: { title: 'some new title', updated_at: update_time } 444 445 expect(response).to have_gitlab_http_status(:ok) 446 expect(json_response['title']).to eq('some new title') 447 expect(Time.parse(json_response['updated_at'])).to be_like_time(update_time) 448 end 449 end 450 end 451 452 describe 'PUT /projects/:id/issues/:issue_iid to update due date' do 453 it 'creates a new project issue' do 454 due_date = 2.weeks.from_now.strftime('%Y-%m-%d') 455 456 put api_for_user, params: { due_date: due_date } 457 458 expect(response).to have_gitlab_http_status(:ok) 459 expect(json_response['due_date']).to eq(due_date) 460 end 461 end 462end 463