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