1# frozen_string_literal: true
2
3require 'spec_helper'
4
5RSpec.describe Issues::CreateService do
6  include AfterNextHelpers
7
8  let_it_be(:group) { create(:group) }
9  let_it_be_with_reload(:project) { create(:project, group: group) }
10  let_it_be(:user) { create(:user) }
11
12  let(:spam_params) { double }
13
14  describe '.rate_limiter_scoped_and_keyed' do
15    it 'is set via the rate_limit call' do
16      expect(described_class.rate_limiter_scoped_and_keyed).to be_a(RateLimitedService::RateLimiterScopedAndKeyed)
17
18      expect(described_class.rate_limiter_scoped_and_keyed.key).to eq(:issues_create)
19      expect(described_class.rate_limiter_scoped_and_keyed.opts[:scope]).to eq(%i[project current_user external_author])
20      expect(described_class.rate_limiter_scoped_and_keyed.rate_limiter_klass).to eq(Gitlab::ApplicationRateLimiter)
21    end
22  end
23
24  describe '#rate_limiter_bypassed' do
25    let(:subject) { described_class.new(project: project, spam_params: {}) }
26
27    it 'is nil by default' do
28      expect(subject.rate_limiter_bypassed).to be_nil
29    end
30  end
31
32  describe '#execute' do
33    let_it_be(:assignee) { create(:user) }
34    let_it_be(:milestone) { create(:milestone, project: project) }
35
36    let(:issue) { described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute }
37
38    before do
39      stub_spam_services
40    end
41
42    context 'when params are valid' do
43      let_it_be(:labels) { create_pair(:label, project: project) }
44
45      before_all do
46        project.add_guest(user)
47        project.add_guest(assignee)
48      end
49
50      let(:opts) do
51        { title: 'Awesome issue',
52          description: 'please fix',
53          assignee_ids: [assignee.id],
54          label_ids: labels.map(&:id),
55          milestone_id: milestone.id,
56          milestone: milestone,
57          due_date: Date.tomorrow }
58      end
59
60      it 'creates the issue with the given params' do
61        expect(Issuable::CommonSystemNotesService).to receive_message_chain(:new, :execute)
62
63        expect(issue).to be_persisted
64        expect(issue.title).to eq('Awesome issue')
65        expect(issue.assignees).to eq([assignee])
66        expect(issue.labels).to match_array(labels)
67        expect(issue.milestone).to eq(milestone)
68        expect(issue.due_date).to eq(Date.tomorrow)
69        expect(issue.work_item_type.base_type).to eq('issue')
70      end
71
72      context 'when skip_system_notes is true' do
73        let(:issue) { described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute(skip_system_notes: true) }
74
75        it 'does not call Issuable::CommonSystemNotesService' do
76          expect(Issuable::CommonSystemNotesService).not_to receive(:new)
77
78          issue
79        end
80      end
81
82      it_behaves_like 'not an incident issue'
83
84      context 'when issue is incident type' do
85        before do
86          opts.merge!(issue_type: 'incident')
87        end
88
89        let(:current_user) { user }
90        let(:incident_label_attributes) { attributes_for(:label, :incident) }
91
92        subject { issue }
93
94        context 'as reporter' do
95          let_it_be(:reporter) { create(:user) }
96
97          let(:user) { reporter }
98
99          before_all do
100            project.add_reporter(reporter)
101          end
102
103          it_behaves_like 'incident issue'
104          it_behaves_like 'has incident label'
105
106          it 'does create an incident label' do
107            expect { subject }
108              .to change { Label.where(incident_label_attributes).count }.by(1)
109          end
110
111          it 'calls IncidentManagement::Incidents::CreateEscalationStatusService' do
112            expect_next(::IncidentManagement::IssuableEscalationStatuses::CreateService, a_kind_of(Issue))
113              .to receive(:execute)
114
115            issue
116          end
117
118          context 'when invalid' do
119            before do
120              opts.merge!(title: '')
121            end
122
123            it 'does not apply an incident label prematurely' do
124              expect { subject }.to not_change(LabelLink, :count).and not_change(Issue, :count)
125            end
126          end
127        end
128
129        context 'as guest' do
130          it_behaves_like 'not an incident issue'
131        end
132      end
133
134      it 'refreshes the number of open issues', :use_clean_rails_memory_store_caching do
135        expect do
136          issue
137
138          BatchLoader::Executor.clear_current
139        end.to change { project.open_issues_count }.from(0).to(1)
140      end
141
142      context 'when current user cannot set issue metadata in the project' do
143        let_it_be(:non_member) { create(:user) }
144
145        it 'filters out params that cannot be set without the :set_issue_metadata permission' do
146          issue = described_class.new(project: project, current_user: non_member, params: opts, spam_params: spam_params).execute
147
148          expect(issue).to be_persisted
149          expect(issue.title).to eq('Awesome issue')
150          expect(issue.description).to eq('please fix')
151          expect(issue.assignees).to be_empty
152          expect(issue.labels).to be_empty
153          expect(issue.milestone).to be_nil
154          expect(issue.due_date).to be_nil
155        end
156
157        it 'can create confidential issues' do
158          issue = described_class.new(project: project, current_user: non_member, params: { confidential: true }, spam_params: spam_params).execute
159
160          expect(issue.confidential).to be_truthy
161        end
162      end
163
164      it 'moves the issue to the end, in an asynchronous worker' do
165        expect(Issues::PlacementWorker).to receive(:perform_async).with(be_nil, Integer)
166
167        described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
168      end
169
170      context 'when label belongs to project group' do
171        let(:group) { create(:group) }
172        let(:group_labels) { create_pair(:group_label, group: group) }
173
174        let(:opts) do
175          {
176            title: 'Title',
177            description: 'Description',
178            label_ids: group_labels.map(&:id)
179          }
180        end
181
182        before do
183          project.update!(group: group)
184        end
185
186        it 'assigns group labels' do
187          expect(issue.labels).to match_array group_labels
188        end
189      end
190
191      context 'when label belongs to different project' do
192        let(:label) { create(:label) }
193
194        let(:opts) do
195          { title: 'Title',
196            description: 'Description',
197            label_ids: [label.id] }
198        end
199
200        it 'does not assign label' do
201          expect(issue.labels).not_to include label
202        end
203      end
204
205      context 'when labels is nil' do
206        let(:opts) do
207          { title: 'Title',
208            description: 'Description',
209            labels: nil }
210        end
211
212        it 'does not assign label' do
213          expect(issue.labels).to be_empty
214        end
215      end
216
217      context 'when labels is nil and label_ids is present' do
218        let(:opts) do
219          { title: 'Title',
220            description: 'Description',
221            labels: nil,
222            label_ids: labels.map(&:id) }
223        end
224
225        it 'assigns group labels' do
226          expect(issue.labels).to match_array labels
227        end
228      end
229
230      context 'when milestone belongs to different project' do
231        let(:milestone) { create(:milestone) }
232
233        let(:opts) do
234          { title: 'Title',
235            description: 'Description',
236            milestone_id: milestone.id }
237        end
238
239        it 'does not assign milestone' do
240          expect(issue.milestone).not_to eq milestone
241        end
242      end
243
244      context 'when assignee is set' do
245        let(:opts) do
246          { title: 'Title',
247            description: 'Description',
248            assignees: [assignee] }
249        end
250
251        it 'invalidates open issues counter for assignees when issue is assigned' do
252          project.add_maintainer(assignee)
253
254          described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
255
256          expect(assignee.assigned_open_issues_count).to eq 1
257        end
258      end
259
260      context 'when duplicate label titles are given' do
261        let(:label) { create(:label, project: project) }
262
263        let(:opts) do
264          { title: 'Title',
265            description: 'Description',
266            labels: [label.title, label.title] }
267        end
268
269        it 'assigns the label once' do
270          expect(issue.labels).to contain_exactly(label)
271        end
272      end
273
274      context 'when sentry identifier is given' do
275        before do
276          sentry_attributes = { sentry_issue_attributes: { sentry_issue_identifier: 42 } }
277          opts.merge!(sentry_attributes)
278        end
279
280        it 'does not assign the sentry error' do
281          expect(issue.sentry_issue).to eq(nil)
282        end
283
284        context 'user is reporter or above' do
285          before do
286            project.add_reporter(user)
287          end
288
289          it 'assigns the sentry error' do
290            expect(issue.sentry_issue).to be_kind_of(SentryIssue)
291          end
292        end
293      end
294
295      it 'executes issue hooks when issue is not confidential' do
296        opts = { title: 'Title', description: 'Description', confidential: false }
297
298        expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :issue_hooks)
299        expect(project).to receive(:execute_integrations).with(an_instance_of(Hash), :issue_hooks)
300
301        described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
302      end
303
304      it 'executes confidential issue hooks when issue is confidential' do
305        opts = { title: 'Title', description: 'Description', confidential: true }
306
307        expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :confidential_issue_hooks)
308        expect(project).to receive(:execute_integrations).with(an_instance_of(Hash), :confidential_issue_hooks)
309
310        described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
311      end
312
313      context 'when rate limiting is in effect', :freeze_time, :clean_gitlab_redis_rate_limiting do
314        let(:user) { create(:user) }
315
316        before do
317          stub_feature_flags(rate_limited_service_issues_create: true)
318          stub_application_setting(issues_create_limit: 1)
319        end
320
321        subject do
322          2.times { described_class.new(project: project, current_user: user, params: opts, spam_params: double).execute }
323        end
324
325        context 'when too many requests are sent by one user' do
326          it 'raises an error' do
327            expect do
328              subject
329            end.to raise_error(RateLimitedService::RateLimitedError)
330          end
331
332          it 'creates 1 issue' do
333            expect do
334              subject
335            rescue RateLimitedService::RateLimitedError
336            end.to change { Issue.count }.by(1)
337          end
338        end
339
340        context 'when limit is higher than count of issues being created' do
341          before do
342            stub_application_setting(issues_create_limit: 2)
343          end
344
345          it 'creates 2 issues' do
346            expect { subject }.to change { Issue.count }.by(2)
347          end
348        end
349      end
350
351      context 'after_save callback to store_mentions' do
352        context 'when mentionable attributes change' do
353          let(:opts) { { title: 'Title', description: "Description with #{user.to_reference}" } }
354
355          it 'saves mentions' do
356            expect_next_instance_of(Issue) do |instance|
357              expect(instance).to receive(:store_mentions!).and_call_original
358            end
359            expect(issue.user_mentions.count).to eq 1
360          end
361        end
362
363        context 'when save fails' do
364          let(:opts) { { title: '', label_ids: labels.map(&:id), milestone_id: milestone.id } }
365
366          it 'does not call store_mentions' do
367            expect_next_instance_of(Issue) do |instance|
368              expect(instance).not_to receive(:store_mentions!).and_call_original
369            end
370            expect(issue.valid?).to be false
371            expect(issue.user_mentions.count).to eq 0
372          end
373        end
374      end
375
376      it 'schedules a namespace onboarding create action worker' do
377        expect(Namespaces::OnboardingIssueCreatedWorker).to receive(:perform_async).with(project.namespace.id)
378
379        issue
380      end
381    end
382
383    context 'issue create service' do
384      context 'assignees' do
385        before_all do
386          project.add_maintainer(user)
387        end
388
389        it 'removes assignee when user id is invalid' do
390          opts = { title: 'Title', description: 'Description', assignee_ids: [-1] }
391
392          issue = described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
393
394          expect(issue.assignees).to be_empty
395        end
396
397        it 'removes assignee when user id is 0' do
398          opts = { title: 'Title', description: 'Description', assignee_ids: [0] }
399
400          issue = described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
401
402          expect(issue.assignees).to be_empty
403        end
404
405        it 'saves assignee when user id is valid' do
406          project.add_maintainer(assignee)
407          opts = { title: 'Title', description: 'Description', assignee_ids: [assignee.id] }
408
409          issue = described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
410
411          expect(issue.assignees).to eq([assignee])
412        end
413
414        context "when issuable feature is private" do
415          before do
416            project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE,
417                                           merge_requests_access_level: ProjectFeature::PRIVATE)
418          end
419
420          levels = [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC]
421
422          levels.each do |level|
423            it "removes not authorized assignee when project is #{Gitlab::VisibilityLevel.level_name(level)}" do
424              project.update!(visibility_level: level)
425              opts = { title: 'Title', description: 'Description', assignee_ids: [assignee.id] }
426
427              issue = described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
428
429              expect(issue.assignees).to be_empty
430            end
431          end
432        end
433      end
434    end
435
436    it_behaves_like 'issuable record that supports quick actions' do
437      let(:issuable) { described_class.new(project: project, current_user: user, params: params, spam_params: spam_params).execute }
438    end
439
440    context 'Quick actions' do
441      context 'with assignee, milestone, and contact in params and command' do
442        let_it_be(:contact) { create(:contact, group: group) }
443
444        let(:opts) do
445          {
446            assignee_ids: [create(:user).id],
447            milestone_id: 1,
448            title: 'Title',
449            description: %(/assign @#{assignee.username}\n/milestone %"#{milestone.name}"),
450            add_contacts: [contact.email]
451          }
452        end
453
454        before_all do
455          group.add_maintainer(user)
456          project.add_maintainer(assignee)
457        end
458
459        it 'assigns, sets milestone, and sets contact to issuable from command' do
460          expect(issue).to be_persisted
461          expect(issue.assignees).to eq([assignee])
462          expect(issue.milestone).to eq(milestone)
463          expect(issue.issue_customer_relations_contacts.last.contact).to eq(contact)
464        end
465      end
466    end
467
468    context 'resolving discussions' do
469      let_it_be(:discussion) { create(:diff_note_on_merge_request).to_discussion }
470      let_it_be(:merge_request) { discussion.noteable }
471      let_it_be(:project) { merge_request.source_project }
472
473      before_all do
474        project.add_maintainer(user)
475      end
476
477      describe 'for a single discussion' do
478        let(:opts) { { discussion_to_resolve: discussion.id, merge_request_to_resolve_discussions_of: merge_request.iid } }
479
480        it 'resolves the discussion' do
481          described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
482          discussion.first_note.reload
483
484          expect(discussion.resolved?).to be(true)
485        end
486
487        it 'added a system note to the discussion' do
488          described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
489
490          reloaded_discussion = MergeRequest.find(merge_request.id).discussions.first
491
492          expect(reloaded_discussion.last_note.system).to eq(true)
493        end
494
495        it 'assigns the title and description for the issue' do
496          issue = described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
497
498          expect(issue.title).not_to be_nil
499          expect(issue.description).not_to be_nil
500        end
501
502        it 'can set nil explicitly to the title and description' do
503          issue = described_class.new(project: project, current_user: user,
504                                      params: {
505                                        merge_request_to_resolve_discussions_of: merge_request,
506                                        description: nil,
507                                        title: nil
508                                      },
509                                      spam_params: spam_params).execute
510
511          expect(issue.description).to be_nil
512          expect(issue.title).to be_nil
513        end
514      end
515
516      describe 'for a merge request' do
517        let(:opts) { { merge_request_to_resolve_discussions_of: merge_request.iid } }
518
519        it 'resolves the discussion' do
520          described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
521          discussion.first_note.reload
522
523          expect(discussion.resolved?).to be(true)
524        end
525
526        it 'added a system note to the discussion' do
527          described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
528
529          reloaded_discussion = MergeRequest.find(merge_request.id).discussions.first
530
531          expect(reloaded_discussion.last_note.system).to eq(true)
532        end
533
534        it 'assigns the title and description for the issue' do
535          issue = described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
536
537          expect(issue.title).not_to be_nil
538          expect(issue.description).not_to be_nil
539        end
540
541        it 'can set nil explicitly to the title and description' do
542          issue = described_class.new(project: project, current_user: user,
543                                      params: {
544                                        merge_request_to_resolve_discussions_of: merge_request,
545                                        description: nil,
546                                        title: nil
547                                      },
548                                      spam_params: spam_params).execute
549
550          expect(issue.description).to be_nil
551          expect(issue.title).to be_nil
552        end
553      end
554    end
555
556    context 'checking spam' do
557      let(:params) do
558        {
559          title: 'Spam issue'
560        }
561      end
562
563      subject do
564        described_class.new(project: project, current_user: user, params: params, spam_params: spam_params)
565      end
566
567      it 'executes SpamActionService' do
568        expect_next_instance_of(
569          Spam::SpamActionService,
570          {
571            spammable: kind_of(Issue),
572            spam_params: spam_params,
573            user: an_instance_of(User),
574            action: :create
575          }
576        ) do |instance|
577          expect(instance).to receive(:execute)
578        end
579
580        subject.execute
581      end
582    end
583  end
584end
585