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