issues_controller_spec.rb 45.3 KB
Newer Older
1 2
# frozen_string_literal: true

3
require 'spec_helper'
4 5

describe Projects::IssuesController do
6 7
  include ProjectForksHelper

8
  let(:project) { create(:project) }
9 10
  let(:user)    { create(:user) }
  let(:issue)   { create(:issue, project: project) }
11

12
  describe "GET #index" do
13
    context 'external issue tracker' do
14 15 16 17
      before do
        sign_in(user)
        project.add_developer(user)
        create(:jira_service, project: project)
18 19
      end

20 21 22 23
      context 'when GitLab issues disabled' do
        it 'returns 404 status' do
          project.issues_enabled = false
          project.save!
24

blackst0ne's avatar
blackst0ne committed
25
          get :index, params: { namespace_id: project.namespace, project_id: project }
26

27
          expect(response).to have_gitlab_http_status(404)
28 29 30 31 32
        end
      end

      context 'when GitLab issues enabled' do
        it 'renders the "index" template' do
blackst0ne's avatar
blackst0ne committed
33
          get :index, params: { namespace_id: project.namespace, project_id: project }
34

35
          expect(response).to have_gitlab_http_status(200)
36 37
          expect(response).to render_template(:index)
        end
38
      end
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63

      context 'when project has moved' do
        let(:new_project) { create(:project) }
        let(:issue) { create(:issue, project: new_project) }

        before do
          project.route.destroy
          new_project.redirect_routes.create!(path: project.full_path)
          new_project.add_developer(user)
        end

        it 'redirects to the new issue tracker from the old one' do
          get :index, params: { namespace_id: project.namespace, project_id: project }

          expect(response).to redirect_to(project_issues_path(new_project))
          expect(response).to have_gitlab_http_status(302)
        end

        it 'redirects from an old issue correctly' do
          get :show, params: { namespace_id: project.namespace, project_id: project, id: issue }

          expect(response).to redirect_to(project_issue_path(new_project, issue))
          expect(response).to have_gitlab_http_status(302)
        end
      end
64 65
    end

66 67 68
    context 'internal issue tracker' do
      before do
        sign_in(user)
69
        project.add_developer(user)
70
      end
71

72 73
      it_behaves_like "issuables list meta-data", :issue

74 75 76
      it_behaves_like 'set sort order from user preference' do
        let(:sorting_param) { 'updated_asc' }
      end
77

78
      it "returns index" do
blackst0ne's avatar
blackst0ne committed
79
        get :index, params: { namespace_id: project.namespace, project_id: project }
80

81
        expect(response).to have_gitlab_http_status(200)
82
      end
83

84
      it "returns 301 if request path doesn't match project path" do
blackst0ne's avatar
blackst0ne committed
85
        get :index, params: { namespace_id: project.namespace, project_id: project.path.upcase }
86

87
        expect(response).to redirect_to(project_issues_path(project))
88
      end
89

90 91
      it "returns 404 when issues are disabled" do
        project.issues_enabled = false
92
        project.save!
93

blackst0ne's avatar
blackst0ne committed
94
        get :index, params: { namespace_id: project.namespace, project_id: project }
95
        expect(response).to have_gitlab_http_status(404)
96 97
      end
    end
98

99
    it_behaves_like 'paginated collection' do
100
      let!(:issue_list) { create_list(:issue, 2, project: project) }
101 102 103 104 105 106 107 108
      let(:collection) { project.issues }
      let(:params) do
        {
          namespace_id: project.namespace.to_param,
          project_id: project,
          state: 'opened'
        }
      end
109 110 111

      before do
        sign_in(user)
112
        project.add_developer(user)
113
        allow(Kaminari.config).to receive(:default_per_page).and_return(1)
114 115
      end

116 117 118
      it 'does not use pagination if disabled' do
        allow(controller).to receive(:pagination_disabled?).and_return(true)

119
        get :index, params: params.merge(page: last_page + 1)
120 121 122 123

        expect(response).to have_gitlab_http_status(200)
        expect(assigns(:issues).size).to eq(2)
      end
124
    end
125

126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156
    context 'with relative_position sorting' do
      let!(:issue_list) { create_list(:issue, 2, project: project) }

      before do
        sign_in(user)
        project.add_developer(user)
        allow(Kaminari.config).to receive(:default_per_page).and_return(1)
      end

      it 'overrides the number allowed on the page' do
        get :index,
          params: {
            namespace_id: project.namespace.to_param,
            project_id:   project,
            sort:         'relative_position'
          }

        expect(assigns(:issues).count).to eq 2
      end

      it 'allows the default number on the page' do
        get :index,
          params: {
            namespace_id: project.namespace.to_param,
            project_id:   project
          }

        expect(assigns(:issues).count).to eq 1
      end
    end

157 158 159 160 161 162 163 164 165 166
    context 'external authorization' do
      before do
        sign_in user
        project.add_developer(user)
      end

      it_behaves_like 'unauthorized when external service denies access' do
        subject { get :index, params: { namespace_id: project.namespace, project_id: project } }
      end
    end
167 168 169
  end

  describe 'GET #new' do
170
    it 'redirects to signin if not logged in' do
blackst0ne's avatar
blackst0ne committed
171
      get :new, params: { namespace_id: project.namespace, project_id: project }
172

173
      expect(flash[:alert]).to eq 'You need to sign in or sign up before continuing.'
174 175 176
      expect(response).to redirect_to(new_user_session_path)
    end

177 178 179
    context 'internal issue tracker' do
      before do
        sign_in(user)
180
        project.add_developer(user)
181 182 183
      end

      it 'builds a new issue' do
blackst0ne's avatar
blackst0ne committed
184
        get :new, params: { namespace_id: project.namespace, project_id: project }
185 186 187 188 189

        expect(assigns(:issue)).to be_a_new(Issue)
      end

      it 'fills in an issue for a merge request' do
190
        project_with_repository = create(:project, :repository)
191
        project_with_repository.add_developer(user)
192 193
        mr = create(:merge_request_with_diff_notes, source_project: project_with_repository)

blackst0ne's avatar
blackst0ne committed
194
        get :new, params: { namespace_id: project_with_repository.namespace, project_id: project_with_repository, merge_request_to_resolve_discussions_of: mr.iid }
195 196 197 198

        expect(assigns(:issue).title).not_to be_empty
        expect(assigns(:issue).description).not_to be_empty
      end
199 200 201 202

      it 'fills in an issue for a discussion' do
        note = create(:note_on_merge_request, project: project)

blackst0ne's avatar
blackst0ne committed
203
        get :new, params: { namespace_id: project.namespace.path, project_id: project, merge_request_to_resolve_discussions_of: note.noteable.iid, discussion_to_resolve: note.discussion_id }
204 205 206 207

        expect(assigns(:issue).title).not_to be_empty
        expect(assigns(:issue).description).not_to be_empty
      end
208 209
    end

210
    context 'external issue tracker' do
211 212 213 214
      let!(:service) do
        create(:custom_issue_tracker_service, project: project, title: 'Custom Issue Tracker', new_issue_url: 'http://test.com')
      end

215 216
      before do
        sign_in(user)
217
        project.add_developer(user)
218 219 220

        external = double
        allow(project).to receive(:external_issue_tracker).and_return(external)
221 222
      end

223 224 225 226
      context 'when GitLab issues disabled' do
        it 'returns 404 status' do
          project.issues_enabled = false
          project.save!
227

blackst0ne's avatar
blackst0ne committed
228
          get :new, params: { namespace_id: project.namespace, project_id: project }
229

230
          expect(response).to have_gitlab_http_status(404)
231 232 233 234 235
        end
      end

      context 'when GitLab issues enabled' do
        it 'renders the "new" template' do
blackst0ne's avatar
blackst0ne committed
236
          get :new, params: { namespace_id: project.namespace, project_id: project }
237

238
          expect(response).to have_gitlab_http_status(200)
239 240
          expect(response).to render_template(:new)
        end
241
      end
242
    end
243 244
  end

245 246 247 248
  # This spec runs as a request-style spec in order to invoke the
  # Rails router. A controller-style spec matches the wrong route, and
  # session['user_return_to'] becomes incorrect.
  describe 'Redirect after sign in', type: :request do
249 250
    context 'with an AJAX request' do
      it 'does not store the visited URL' do
251
        get project_issue_path(project, issue), xhr: true
252 253 254 255 256 257 258

        expect(session['user_return_to']).to be_blank
      end
    end

    context 'without an AJAX request' do
      it 'stores the visited URL' do
259
        get project_issue_path(project, issue)
260

261
        expect(session['user_return_to']).to eq(project_issue_path(project, issue))
262 263 264 265
      end
    end
  end

266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282
  describe 'POST #move' do
    before do
      sign_in(user)
      project.add_developer(user)
    end

    context 'when moving issue to another private project' do
      let(:another_project) { create(:project, :private) }

      context 'when user has access to move issue' do
        before do
          another_project.add_reporter(user)
        end

        it 'moves issue to another project' do
          move_issue

283
          expect(response).to have_gitlab_http_status :ok
284 285 286 287 288 289 290 291
          expect(another_project.issues).not_to be_empty
        end
      end

      context 'when user does not have access to move issue' do
        it 'responds with 404' do
          move_issue

292
          expect(response).to have_gitlab_http_status :not_found
293 294
        end
      end
295

296
      def move_issue
297
        post :move,
blackst0ne's avatar
blackst0ne committed
298 299 300 301 302 303 304
          params: {
            namespace_id: project.namespace.to_param,
            project_id: project,
            id: issue.iid,
            move_to_project_id: another_project.id
          },
          format: :json
305 306 307 308
      end
    end
  end

309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392
  describe 'PUT #reorder' do
    let(:group)   { create(:group, projects: [project]) }
    let!(:issue1) { create(:issue, project: project, relative_position: 10) }
    let!(:issue2) { create(:issue, project: project, relative_position: 20) }
    let!(:issue3) { create(:issue, project: project, relative_position: 30) }

    before do
      sign_in(user)
    end

    context 'when user has access' do
      before do
        project.add_developer(user)
      end

      context 'with valid params' do
        it 'reorders issues and returns a successful 200 response' do
          reorder_issue(issue1,
            move_after_id: issue2.id,
            move_before_id: issue3.id,
            group_full_path: group.full_path)

          [issue1, issue2, issue3].map(&:reload)

          expect(response).to have_gitlab_http_status(200)
          expect(issue1.relative_position)
            .to be_between(issue2.relative_position, issue3.relative_position)
        end
      end

      context 'with invalid params' do
        it 'returns a unprocessable entity 422 response for invalid move ids' do
          reorder_issue(issue1, move_after_id: 99, move_before_id: 999)

          expect(response).to have_gitlab_http_status(422)
        end

        it 'returns a not found 404 response for invalid issue id' do
          reorder_issue(object_double(issue1, iid: 999),
            move_after_id: issue2.id,
            move_before_id: issue3.id)

          expect(response).to have_gitlab_http_status(404)
        end

        it 'returns a unprocessable entity 422 response for issues not in group' do
          another_group = create(:group)

          reorder_issue(issue1,
            move_after_id: issue2.id,
            move_before_id: issue3.id,
            group_full_path: another_group.full_path)

          expect(response).to have_gitlab_http_status(422)
        end
      end
    end

    context 'with unauthorized user' do
      before do
        project.add_guest(user)
      end

      it 'responds with 404' do
        reorder_issue(issue1, move_after_id: issue2.id, move_before_id: issue3.id)

        expect(response).to have_gitlab_http_status(:not_found)
      end
    end

    def reorder_issue(issue, move_after_id: nil, move_before_id: nil, group_full_path: nil)
      put :reorder,
           params: {
               namespace_id: project.namespace.to_param,
               project_id: project,
               id: issue.iid,
               move_after_id: move_after_id,
               move_before_id: move_before_id,
               group_full_path: group_full_path
           },
           format: :json
    end
  end

393 394 395
  describe 'PUT #update' do
    subject do
      put :update,
blackst0ne's avatar
blackst0ne committed
396 397 398 399 400 401 402
        params: {
          namespace_id: project.namespace,
          project_id: project,
          id: issue.to_param,
          issue: { title: 'New title' }
        },
        format: :json
403 404 405 406 407 408 409 410
    end

    before do
      sign_in(user)
    end

    context 'when user has access to update issue' do
      before do
411
        project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
412 413 414 415 416 417 418 419 420
        project.add_developer(user)
      end

      it 'updates the issue' do
        subject

        expect(response).to have_http_status(:ok)
        expect(issue.reload.title).to eq('New title')
      end
421 422 423 424

      context 'when Akismet is enabled and the issue is identified as spam' do
        before do
          stub_application_setting(recaptcha_enabled: true)
425 426 427 428 429 430 431 432 433 434 435 436 437 438 439
          expect_next_instance_of(AkismetService) do |akismet_service|
            expect(akismet_service).to receive_messages(spam?: true)
          end
        end

        context 'when allow_possible_spam feature flag is false' do
          before do
            stub_feature_flags(allow_possible_spam: false)
          end

          it 'renders json with recaptcha_html' do
            subject

            expect(json_response).to have_key('recaptcha_html')
          end
440 441
        end

442 443 444
        context 'when allow_possible_spam feature flag is true' do
          it 'updates the issue' do
            subject
445

446 447 448
            expect(response).to have_http_status(:ok)
            expect(issue.reload.title).to eq('New title')
          end
449 450
        end
      end
451 452 453 454 455 456 457 458 459 460 461 462 463 464 465
    end

    context 'when user does not have access to update issue' do
      before do
        project.add_guest(user)
      end

      it 'responds with 404' do
        subject

        expect(response).to have_http_status(:not_found)
      end
    end
  end

466 467 468
  describe 'GET #realtime_changes' do
    def go(id:)
      get :realtime_changes,
blackst0ne's avatar
blackst0ne committed
469 470 471 472 473
        params: {
          namespace_id: project.namespace.to_param,
          project_id: project,
          id: id
        }
474 475 476 477 478 479 480 481 482 483 484 485 486 487
    end

    context 'when an issue was edited' do
      before do
        project.add_developer(user)

        issue.update!(last_edited_by: user, last_edited_at: issue.created_at + 1.minute)

        sign_in(user)
      end

      it 'returns last edited time' do
        go(id: issue.iid)

488 489
        expect(json_response).to include('updated_at')
        expect(json_response['updated_at']).to eq(issue.last_edited_at.to_time.iso8601)
490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510
      end
    end

    context 'when an issue was edited by a deleted user' do
      let(:deleted_user) { create(:user) }

      before do
        project.add_developer(user)

        issue.update!(last_edited_by: deleted_user, last_edited_at: Time.now)

        deleted_user.destroy
        sign_in(user)
      end

      it 'returns 200' do
        go(id: issue.iid)

        expect(response).to have_gitlab_http_status(200)
      end
    end
511 512 513 514 515 516 517 518 519 520 521

    context 'when getting the changes' do
      before do
        project.add_developer(user)

        sign_in(user)
      end

      it 'returns the necessary data' do
        go(id: issue.iid)

522 523
        expect(json_response).to include('title_text', 'description', 'description_text')
        expect(json_response).to include('task_status', 'lock_version')
524 525
      end
    end
526 527
  end

528
  describe 'Confidential Issues' do
529
    let(:project) { create(:project_empty_repo, :public) }
530 531 532 533 534 535 536
    let(:assignee) { create(:assignee) }
    let(:author) { create(:user) }
    let(:non_member) { create(:user) }
    let(:member) { create(:user) }
    let(:admin) { create(:admin) }
    let!(:issue) { create(:issue, project: project) }
    let!(:unescaped_parameter_value) { create(:issue, :confidential, project: project, author: author) }
537
    let!(:request_forgery_timing_attack) { create(:issue, :confidential, project: project, assignees: [assignee]) }
538 539

    describe 'GET #index' do
540
      it 'does not list confidential issues for guests' do
541 542 543 544 545 546
        sign_out(:user)
        get_issues

        expect(assigns(:issues)).to eq [issue]
      end

547
      it 'does not list confidential issues for non project members' do
548 549 550 551 552 553
        sign_in(non_member)
        get_issues

        expect(assigns(:issues)).to eq [issue]
      end

554
      it 'does not list confidential issues for project members with guest role' do
555
        sign_in(member)
556
        project.add_guest(member)
557 558 559 560 561 562

        get_issues

        expect(assigns(:issues)).to eq [issue]
      end

563
      it 'lists confidential issues for author' do
564 565 566 567 568 569 570
        sign_in(author)
        get_issues

        expect(assigns(:issues)).to include unescaped_parameter_value
        expect(assigns(:issues)).not_to include request_forgery_timing_attack
      end

571
      it 'lists confidential issues for assignee' do
572 573 574 575 576 577 578
        sign_in(assignee)
        get_issues

        expect(assigns(:issues)).not_to include unescaped_parameter_value
        expect(assigns(:issues)).to include request_forgery_timing_attack
      end

579
      it 'lists confidential issues for project members' do
580
        sign_in(member)
581
        project.add_developer(member)
582 583 584 585 586 587 588

        get_issues

        expect(assigns(:issues)).to include unescaped_parameter_value
        expect(assigns(:issues)).to include request_forgery_timing_attack
      end

589
      it 'lists confidential issues for admin' do
590 591 592 593 594 595 596 597 598
        sign_in(admin)
        get_issues

        expect(assigns(:issues)).to include unescaped_parameter_value
        expect(assigns(:issues)).to include request_forgery_timing_attack
      end

      def get_issues
        get :index,
blackst0ne's avatar
blackst0ne committed
599 600 601 602
          params: {
            namespace_id: project.namespace.to_param,
            project_id: project
          }
603 604
      end
    end
605

606 607
    shared_examples_for 'restricted action' do |http_status|
      it 'returns 404 for guests' do
608
        sign_out(:user)
609 610
        go(id: unescaped_parameter_value.to_param)

611
        expect(response).to have_gitlab_http_status :not_found
612 613 614 615 616 617
      end

      it 'returns 404 for non project members' do
        sign_in(non_member)
        go(id: unescaped_parameter_value.to_param)

618
        expect(response).to have_gitlab_http_status :not_found
619 620 621 622
      end

      it 'returns 404 for project members with guest role' do
        sign_in(member)
623
        project.add_guest(member)
624 625
        go(id: unescaped_parameter_value.to_param)

626
        expect(response).to have_gitlab_http_status :not_found
627 628 629 630 631 632
      end

      it "returns #{http_status[:success]} for author" do
        sign_in(author)
        go(id: unescaped_parameter_value.to_param)

633
        expect(response).to have_gitlab_http_status http_status[:success]
634 635 636 637 638 639
      end

      it "returns #{http_status[:success]} for assignee" do
        sign_in(assignee)
        go(id: request_forgery_timing_attack.to_param)

640
        expect(response).to have_gitlab_http_status http_status[:success]
641 642 643 644
      end

      it "returns #{http_status[:success]} for project members" do
        sign_in(member)
645
        project.add_developer(member)
646 647
        go(id: unescaped_parameter_value.to_param)

648
        expect(response).to have_gitlab_http_status http_status[:success]
649 650 651 652 653 654
      end

      it "returns #{http_status[:success]} for admin" do
        sign_in(admin)
        go(id: unescaped_parameter_value.to_param)

655
        expect(response).to have_gitlab_http_status http_status[:success]
656 657 658
      end
    end

659 660 661 662 663 664 665 666 667 668 669
    describe 'PUT #update' do
      def update_issue(issue_params: {}, additional_params: {}, id: nil)
        id ||= issue.iid
        params = {
          namespace_id: project.namespace.to_param,
          project_id: project,
          id: id,
          issue: { title: 'New title' }.merge(issue_params),
          format: :json
        }.merge(additional_params)

blackst0ne's avatar
blackst0ne committed
670
        put :update, params: params
671 672 673 674 675 676 677 678
      end

      def go(id:)
        update_issue(id: id)
      end

      before do
        sign_in(user)
679
        project.add_developer(user)
680 681 682 683 684 685 686 687 688 689 690 691
      end

      it_behaves_like 'restricted action', success: 200
      it_behaves_like 'update invalid issuable', Issue

      context 'changing the assignee' do
        it 'limits the attributes exposed on the assignee' do
          assignee = create(:user)
          project.add_developer(assignee)

          update_issue(issue_params: { assignee_ids: [assignee.id] })

692
          expect(json_response['assignees'].first.keys)
693 694 695 696 697 698 699 700 701 702 703 704
            .to match_array(%w(id name username avatar_url state web_url))
        end
      end

      context 'Akismet is enabled' do
        before do
          project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
          stub_application_setting(recaptcha_enabled: true)
        end

        context 'when an issue is not identified as spam' do
          before do
705 706 707
            expect_next_instance_of(AkismetService) do |akismet_service|
              expect(akismet_service).to receive_messages(spam?: false)
            end
708 709 710 711 712 713 714 715 716 717
          end

          it 'normally updates the issue' do
            expect { update_issue(issue_params: { title: 'Foo' }) }.to change { issue.reload.title }.to('Foo')
          end
        end

        context 'when an issue is identified as spam' do
          context 'when captcha is not verified' do
            before do
718 719 720
              expect_next_instance_of(AkismetService) do |akismet_service|
                expect(akismet_service).to receive_messages(spam?: true)
              end
721 722
            end

723 724 725 726
            context 'when allow_possible_spam feature flag is false' do
              before do
                stub_feature_flags(allow_possible_spam: false)
              end
727

728 729 730
              it 'rejects an issue recognized as a spam' do
                expect { update_issue }.not_to change { issue.reload.title }
              end
731

732 733
              it 'rejects an issue recognized as a spam when recaptcha disabled' do
                stub_application_setting(recaptcha_enabled: false)
734

735 736
                expect { update_issue }.not_to change { issue.reload.title }
              end
737

738 739 740 741
              it 'creates a spam log' do
                expect { update_issue(issue_params: { title: 'Spam title' }) }
                  .to log_spam(title: 'Spam title', noteable_type: 'Issue')
              end
742

743 744 745 746 747
              it 'renders recaptcha_html json response' do
                update_issue

                expect(json_response).to have_key('recaptcha_html')
              end
748

749 750
              it 'returns 200 status' do
                update_issue
751

752 753
                expect(response).to have_gitlab_http_status(200)
              end
754 755
            end

756 757 758 759
            context 'when allow_possible_spam feature flag is true' do
              it 'updates the issue recognized as spam' do
                expect { update_issue }.to change { issue.reload.title }
              end
760

761 762 763 764 765 766 767 768 769 770 771 772 773
              it 'creates a spam log' do
                expect { update_issue(issue_params: { title: 'Spam title' }) }
                  .to log_spam(
                    title: 'Spam title', description: issue.description,
                    noteable_type: 'Issue', recaptcha_verified: false
                  )
              end

              it 'returns 200 status' do
                update_issue

                expect(response).to have_gitlab_http_status(200)
              end
774 775 776 777 778 779 780 781 782 783 784 785 786 787
            end
          end

          context 'when captcha is verified' do
            let(:spammy_title) { 'Whatever' }
            let!(:spam_logs) { create_list(:spam_log, 2, user: user, title: spammy_title) }

            def update_verified_issue
              update_issue(
                issue_params: { title: spammy_title },
                additional_params: { spam_log_id: spam_logs.last.id, recaptcha_verification: true })
            end

            it 'returns 200 status' do
788
              expect(response).to have_gitlab_http_status(200)
789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809
            end

            it 'accepts an issue after recaptcha is verified' do
              expect { update_verified_issue }.to change { issue.reload.title }.to(spammy_title)
            end

            it 'marks spam log as recaptcha_verified' do
              expect { update_verified_issue }.to change { SpamLog.last.recaptcha_verified }.from(false).to(true)
            end

            it 'does not mark spam log as recaptcha_verified when it does not belong to current_user' do
              spam_log = create(:spam_log)

              expect { update_issue(issue_params: { spam_log_id: spam_log.id, recaptcha_verification: true }) }
                .not_to change { SpamLog.last.recaptcha_verified }
            end
          end
        end
      end
    end

810 811 812 813 814
    describe 'GET #show' do
      it_behaves_like 'restricted action', success: 200

      def go(id:)
        get :show,
blackst0ne's avatar
blackst0ne committed
815 816 817 818 819
          params: {
            namespace_id: project.namespace.to_param,
            project_id: project,
            id: id
          }
820
      end
821

822
      it 'avoids (most) N+1s loading labels', :request_store do
823 824 825 826 827 828
        label = create(:label, project: project).to_reference
        labels = create_list(:label, 10, project: project).map(&:to_reference)
        issue = create(:issue, project: project, description: 'Test issue')

        control_count = ActiveRecord::QueryRecorder.new { issue.update(description: [issue.description, label].join(' ')) }.count

829
        # Follow-up to get rid of this `2 * label.count` requirement: https://gitlab.com/gitlab-org/gitlab-foss/issues/52230
830 831 832
        expect { issue.update(description: [issue.description, labels].join(' ')) }
          .not_to exceed_query_limit(control_count + 2 * labels.count)
      end
833 834
    end

835 836 837 838 839
    describe 'GET #realtime_changes' do
      it_behaves_like 'restricted action', success: 200

      def go(id:)
        get :realtime_changes,
blackst0ne's avatar
blackst0ne committed
840 841 842 843 844
          params: {
            namespace_id: project.namespace.to_param,
            project_id: project,
            id: id
          }
845 846
      end
    end
847 848 849 850 851 852

    describe 'GET #edit' do
      it_behaves_like 'restricted action', success: 200

      def go(id:)
        get :edit,
blackst0ne's avatar
blackst0ne committed
853 854 855 856 857
          params: {
            namespace_id: project.namespace.to_param,
            project_id: project,
            id: id
          }
858 859 860 861 862 863 864 865
      end
    end

    describe 'PUT #update' do
      it_behaves_like 'restricted action', success: 302

      def go(id:)
        put :update,
blackst0ne's avatar
blackst0ne committed
866 867 868 869 870 871
          params: {
            namespace_id: project.namespace.to_param,
            project_id: project,
            id: id,
            issue: { title: 'New title' }
          }
872 873
      end
    end
874
  end
875

876
  describe 'POST #create' do
877
    def post_new_issue(issue_attrs = {}, additional_params = {})
878
      sign_in(user)
879
      project = create(:project, :public)
880
      project.add_developer(user)
881

blackst0ne's avatar
blackst0ne committed
882
      post :create, params: {
883
        namespace_id: project.namespace.to_param,
884
        project_id: project,
885 886
        issue: { title: 'Title', description: 'Description' }.merge(issue_attrs)
      }.merge(additional_params)
887 888 889 890

      project.issues.first
    end

891
    context 'resolving discussions in MergeRequest' do
892
      let(:discussion) { create(:diff_note_on_merge_request).to_discussion }
893 894 895 896
      let(:merge_request) { discussion.noteable }
      let(:project) { merge_request.source_project }

      before do
897
        project.add_maintainer(user)
898 899 900 901
        sign_in user
      end

      let(:merge_request_params) do
Bob Van Landuyt's avatar
Bob Van Landuyt committed
902
        { merge_request_to_resolve_discussions_of: merge_request.iid }
903 904
      end

905
      def post_issue(issue_params, other_params: {})
blackst0ne's avatar
blackst0ne committed
906
        post :create, params: { namespace_id: project.namespace.to_param, project_id: project, issue: issue_params, merge_request_to_resolve_discussions_of: merge_request.iid }.merge(other_params)
907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924
      end

      it 'creates an issue for the project' do
        expect { post_issue({ title: 'Hello' }) }.to change { project.issues.reload.size }.by(1)
      end

      it "doesn't overwrite given params" do
        post_issue(description: 'Manually entered description')

        expect(assigns(:issue).description).to eq('Manually entered description')
      end

      it 'resolves the discussion in the merge_request' do
        post_issue(title: 'Hello')
        discussion.first_note.reload

        expect(discussion.resolved?).to eq(true)
      end
925

926 927 928
      it 'sets a flash message' do
        post_issue(title: 'Hello')

929
        expect(flash[:notice]).to eq('Resolved all discussions.')
930 931
      end

932 933 934 935 936 937
      describe "resolving a single discussion" do
        before do
          post_issue({ title: 'Hello' }, other_params: { discussion_to_resolve: discussion.id })
        end
        it 'resolves a single discussion' do
          discussion.first_note.reload
938

939 940
          expect(discussion.resolved?).to eq(true)
        end
941

942 943 944
        it 'sets a flash message that one discussion was resolved' do
          expect(flash[:notice]).to eq('Resolved 1 discussion.')
        end
945
      end
946 947
    end

948 949
    context 'Akismet is enabled' do
      before do
950
        stub_application_setting(recaptcha_enabled: true)
951 952
      end

953
      context 'when an issue is not identified as spam' do
954
        before do
955 956 957 958 959
          stub_feature_flags(allow_possible_spam: false)

          expect_next_instance_of(AkismetService) do |akismet_service|
            expect(akismet_service).to receive_messages(spam?: false)
          end
960
        end
961

962 963
        it 'creates an issue' do
          expect { post_new_issue(title: 'Some title') }.to change(Issue, :count)
964
        end
965 966
      end

967
      context 'when an issue is identified as spam' do
968 969 970 971 972
        context 'when captcha is not verified' do
          def post_spam_issue
            post_new_issue(title: 'Spam Title', description: 'Spam lives here')
          end

973
          before do
974 975 976
            expect_next_instance_of(AkismetService) do |akismet_service|
              expect(akismet_service).to receive_messages(spam?: true)
            end
977
          end
978

979 980 981 982
          context 'when allow_possible_spam feature flag is false' do
            before do
              stub_feature_flags(allow_possible_spam: false)
            end
983

984 985 986
            it 'rejects an issue recognized as a spam' do
              expect { post_spam_issue }.not_to change(Issue, :count)
            end
987

988 989 990 991
            it 'creates a spam log' do
              expect { post_spam_issue }
                .to log_spam(title: 'Spam Title', noteable_type: 'Issue', recaptcha_verified: false)
            end
992

993 994 995 996 997 998 999 1000 1001
            it 'does not create an issue when it is not valid' do
              expect { post_new_issue(title: '') }.not_to change(Issue, :count)
            end

            it 'does not create an issue when recaptcha is not enabled' do
              stub_application_setting(recaptcha_enabled: false)

              expect { post_spam_issue }.not_to change(Issue, :count)
            end
1002 1003
          end

1004 1005 1006 1007
          context 'when allow_possible_spam feature flag is true' do
            it 'creates an issue recognized as spam' do
              expect { post_spam_issue }.to change(Issue, :count)
            end
1008

1009 1010 1011 1012 1013 1014 1015 1016
            it 'creates a spam log' do
              expect { post_spam_issue }
                .to log_spam(title: 'Spam Title', noteable_type: 'Issue', recaptcha_verified: false)
            end

            it 'does not create an issue when it is not valid' do
              expect { post_new_issue(title: '') }.not_to change(Issue, :count)
            end
1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027
          end
        end

        context 'when captcha is verified' do
          let!(:spam_logs) { create_list(:spam_log, 2, user: user, title: 'Title') }

          def post_verified_issue
            post_new_issue({}, { spam_log_id: spam_logs.last.id, recaptcha_verification: true } )
          end

          before do
1028
            expect(controller).to receive_messages(verify_recaptcha: true)
1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041
          end

          it 'accepts an issue after recaptcha is verified' do
            expect { post_verified_issue }.to change(Issue, :count)
          end

          it 'marks spam log as recaptcha_verified' do
            expect { post_verified_issue }.to change { SpamLog.last.recaptcha_verified }.from(false).to(true)
          end

          it 'does not mark spam log as recaptcha_verified when it does not belong to current_user' do
            spam_log = create(:spam_log)

1042 1043
            expect { post_new_issue({}, { spam_log_id: spam_log.id, recaptcha_verification: true } ) }
              .not_to change { SpamLog.last.recaptcha_verified }
1044 1045
          end
        end
1046 1047
      end
    end
1048 1049 1050 1051 1052 1053

    context 'user agent details are saved' do
      before do
        request.env['action_dispatch.remote_ip'] = '127.0.0.1'
      end

1054
      it 'creates a user agent detail' do
1055
        expect { post_new_issue }.to change(UserAgentDetail, :count).by(1)
1056 1057 1058
      end
    end

1059
    context 'when description has quick actions' do
1060
      before do
1061 1062 1063
        sign_in(user)
      end

1064 1065 1066 1067 1068 1069 1070 1071 1072 1073
      it 'can add spent time' do
        issue = post_new_issue(description: '/spend 1h')

        expect(issue.total_time_spent).to eq(3600)
      end

      it 'can set the time estimate' do
        issue = post_new_issue(description: '/estimate 2h')

        expect(issue.time_estimate).to eq(7200)
1074 1075
      end
    end
1076 1077
  end

1078 1079 1080
  describe 'POST #mark_as_spam' do
    context 'properly submits to Akismet' do
      before do
1081 1082 1083 1084 1085 1086
        expect_next_instance_of(AkismetService) do |akismet_service|
          expect(akismet_service).to receive_messages(submit_spam: true)
        end
        expect_next_instance_of(ApplicationSetting) do |setting|
          expect(setting).to receive_messages(akismet_enabled: true)
        end
1087 1088 1089 1090 1091
      end

      def post_spam
        admin = create(:admin)
        create(:user_agent_detail, subject: issue)
1092
        project.add_maintainer(admin)
1093
        sign_in(admin)
blackst0ne's avatar
blackst0ne committed
1094
        post :mark_as_spam, params: {
1095 1096
          namespace_id: project.namespace,
          project_id: project,
1097 1098 1099 1100 1101 1102
          id: issue.iid
        }
      end

      it 'updates issue' do
        post_spam
1103
        expect(issue.submittable_as_spam?).to be_falsey
1104 1105 1106 1107
      end
    end
  end

1108
  describe "DELETE #destroy" do
1109
    context "when the user is a developer" do
1110 1111 1112 1113
      before do
        sign_in(user)
      end

1114
      it "rejects a developer to destroy an issue" do
blackst0ne's avatar
blackst0ne committed
1115
        delete :destroy, params: { namespace_id: project.namespace, project_id: project, id: issue.iid }
1116
        expect(response).to have_gitlab_http_status(404)
1117
      end
1118 1119
    end

1120 1121 1122
    context "when the user is owner" do
      let(:owner)     { create(:user) }
      let(:namespace) { create(:namespace, owner: owner) }
1123
      let(:project)   { create(:project, namespace: namespace) }
1124

1125 1126 1127
      before do
        sign_in(owner)
      end
1128

1129
      it "deletes the issue" do
1130
        delete :destroy, params: { namespace_id: project.namespace, project_id: project, id: issue.iid, destroy_confirm: true }
1131

1132
        expect(response).to have_gitlab_http_status(302)
1133
        expect(controller).to set_flash[:notice].to(/The issue was successfully deleted\./)
1134
      end
1135

1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160
      it "deletes the issue" do
        delete :destroy, params: { namespace_id: project.namespace, project_id: project, id: issue.iid, destroy_confirm: true }

        expect(response).to have_gitlab_http_status(302)
        expect(controller).to set_flash[:notice].to(/The issue was successfully deleted\./)
      end

      it "prevents deletion if destroy_confirm is not set" do
        expect(Gitlab::Sentry).to receive(:track_acceptable_exception).and_call_original

        delete :destroy, params: { namespace_id: project.namespace, project_id: project, id: issue.iid }

        expect(response).to have_gitlab_http_status(302)
        expect(controller).to set_flash[:notice].to('Destroy confirmation not provided for issue')
      end

      it "prevents deletion in JSON format if destroy_confirm is not set" do
        expect(Gitlab::Sentry).to receive(:track_acceptable_exception).and_call_original

        delete :destroy, params: { namespace_id: project.namespace, project_id: project, id: issue.iid, format: 'json' }

        expect(response).to have_gitlab_http_status(422)
        expect(json_response).to eq({ 'errors' => 'Destroy confirmation not provided for issue' })
      end

1161
      it 'delegates the update of the todos count cache to TodoService' do
1162
        expect_any_instance_of(TodoService).to receive(:destroy_target).with(issue).once
1163

1164
        delete :destroy, params: { namespace_id: project.namespace, project_id: project, id: issue.iid, destroy_confirm: true }
1165
      end
1166 1167
    end
  end
1168 1169 1170 1171

  describe 'POST #toggle_award_emoji' do
    before do
      sign_in(user)
1172
      project.add_developer(user)
1173 1174
    end

1175 1176 1177 1178 1179 1180 1181 1182
    subject do
      post(:toggle_award_emoji, params: {
        namespace_id: project.namespace,
        project_id: project,
        id: issue.iid,
        name: emoji_name
      })
    end
1183

1184 1185
    let(:emoji_name) { 'thumbsup' }

1186
    it "toggles the award emoji" do
Z.J. van de Weg's avatar
Z.J. van de Weg committed
1187
      expect do
1188
        subject
Z.J. van de Weg's avatar
Z.J. van de Weg committed
1189
      end.to change { issue.award_emoji.count }.by(1)
1190

1191
      expect(response).to have_gitlab_http_status(200)
1192
    end
1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208

    it "removes the already awarded emoji" do
      create(:award_emoji, awardable: issue, name: emoji_name, user: user)

      expect { subject }.to change { AwardEmoji.count }.by(-1)

      expect(response).to have_gitlab_http_status(200)
    end

    it 'marks Todos on the Issue as done' do
      todo = create(:todo, target: issue, project: project, user: user)

      subject

      expect(todo.reload).to be_done
    end
1209
  end
1210 1211

  describe 'POST create_merge_request' do
1212
    let(:target_project_id) { nil }
1213
    let(:project) { create(:project, :repository, :public) }
1214

1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229
    before do
      project.add_developer(user)
      sign_in(user)
    end

    it 'creates a new merge request' do
      expect { create_merge_request }.to change(project.merge_requests, :count).by(1)
    end

    it 'render merge request as json' do
      create_merge_request

      expect(response).to match_response_schema('merge_request')
    end

1230
    it 'is not available when the project is archived' do
1231
      project.update!(archived: true)
1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245

      create_merge_request

      expect(response).to have_gitlab_http_status(404)
    end

    it 'is not available for users who cannot create merge requests' do
      sign_in(create(:user))

      create_merge_request

      expect(response).to have_gitlab_http_status(404)
    end

1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270
    context 'target_project_id is set' do
      let(:target_project) { fork_project(project, user, repository: true) }
      let(:target_project_id) { target_project.id }

      context 'create_confidential_merge_request feature is enabled' do
        before do
          stub_feature_flags(create_confidential_merge_request: true)
        end

        it 'creates a new merge request' do
          expect { create_merge_request }.to change(target_project.merge_requests, :count).by(1)
        end
      end

      context 'create_confidential_merge_request feature is disabled' do
        before do
          stub_feature_flags(create_confidential_merge_request: false)
        end

        it 'creates a new merge request' do
          expect { create_merge_request }.to change(project.merge_requests, :count).by(1)
        end
      end
    end

1271
    def create_merge_request
1272 1273 1274 1275 1276 1277 1278 1279 1280 1281
      post(
        :create_merge_request,
        params: {
          namespace_id: project.namespace.to_param,
          project_id: project.to_param,
          id: issue.to_param,
          target_project_id: target_project_id
        },
        format: :json
      )
1282 1283
    end
  end
1284

Heinrich Lee Yu's avatar
Heinrich Lee Yu committed
1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321
  describe 'POST #import_csv' do
    let(:project) { create(:project, :public) }
    let(:file) { fixture_file_upload('spec/fixtures/csv_comma.csv') }

    context 'unauthorized' do
      it 'returns 404 for guests' do
        sign_out(:user)

        import_csv

        expect(response).to have_gitlab_http_status :not_found
      end

      it 'returns 404 for project members with reporter role' do
        sign_in(user)
        project.add_reporter(user)

        import_csv

        expect(response).to have_gitlab_http_status :not_found
      end
    end

    context 'authorized' do
      before do
        sign_in(user)
        project.add_developer(user)
      end

      it "returns 302 for project members with developer role" do
        import_csv

        expect(flash[:notice]).to include('Your issues are being imported')
        expect(response).to redirect_to(project_issues_path(project))
      end

      it "shows error when upload fails" do
1322 1323 1324
        expect_next_instance_of(UploadService) do |upload_service|
          expect(upload_service).to receive(:execute).and_return(nil)
        end
Heinrich Lee Yu's avatar
Heinrich Lee Yu committed
1325 1326 1327 1328 1329 1330 1331 1332 1333

        import_csv

        expect(flash[:alert]).to include('File upload error.')
        expect(response).to redirect_to(project_issues_path(project))
      end
    end

    def import_csv
1334 1335 1336
      post :import_csv, params: { namespace_id: project.namespace.to_param,
                                  project_id: project.to_param,
                                  file: file }
Heinrich Lee Yu's avatar
Heinrich Lee Yu committed
1337 1338 1339
    end
  end

1340 1341
  describe 'GET #discussions' do
    let!(:discussion) { create(:discussion_note_on_issue, noteable: issue, project: issue.project) }
1342
    context 'when authenticated' do
1343
      before do
1344 1345
        project.add_developer(user)
        sign_in(user)
1346 1347
      end

1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369
      context do
        it_behaves_like 'discussions provider' do
          let!(:author) { create(:user) }
          let!(:project) { create(:project) }

          let!(:issue) { create(:issue, project: project, author: user) }

          let!(:note_on_issue1) { create(:discussion_note_on_issue, noteable: issue, project: issue.project, author: create(:user)) }
          let!(:note_on_issue2) { create(:discussion_note_on_issue, noteable: issue, project: issue.project, author: create(:user)) }

          let(:requested_iid) { issue.iid }
          let(:expected_discussion_count) { 3 }
          let(:expected_discussion_ids) do
            [
              issue.notes.first.discussion_id,
              note_on_issue1.discussion_id,
              note_on_issue2.discussion_id
            ]
          end
        end
      end

1370
      it 'returns discussion json' do
blackst0ne's avatar
blackst0ne committed
1371
        get :discussions, params: { namespace_id: project.namespace, project_id: project, id: issue.iid }
1372

Felipe Artur's avatar
Felipe Artur committed
1373
        expect(json_response.first.keys).to match_array(%w[id reply_id expanded notes diff_discussion discussion_path individual_note resolvable resolved resolved_at resolved_by resolved_by_push commit_id for_commit project_id])
1374 1375
      end

1376 1377 1378
      it 'renders the author status html if there is a status' do
        create(:user_status, user: discussion.author)

blackst0ne's avatar
blackst0ne committed
1379
        get :discussions, params: { namespace_id: project.namespace, project_id: project, id: issue.iid }
1380 1381 1382 1383 1384 1385 1386 1387

        note_json = json_response.first['notes'].first

        expect(note_json['author']['status_tooltip_html']).to be_present
      end

      it 'does not cause an extra query for the status' do
        control = ActiveRecord::QueryRecorder.new do
blackst0ne's avatar
blackst0ne committed
1388
          get :discussions, params: { namespace_id: project.namespace, project_id: project, id: issue.iid }
1389 1390 1391 1392 1393 1394
        end

        create(:user_status, user: discussion.author)
        second_discussion = create(:discussion_note_on_issue, noteable: issue, project: issue.project, author: create(:user))
        create(:user_status, user: second_discussion.author)

blackst0ne's avatar
blackst0ne committed
1395
        expect { get :discussions, params: { namespace_id: project.namespace, project_id: project, id: issue.iid } }
1396 1397 1398
          .not_to exceed_query_limit(control)
      end

1399 1400
      context 'when user is setting notes filters' do
        let(:issuable) { issue }
1401
        let(:issuable_parent) { project }
1402 1403 1404 1405 1406
        let!(:discussion_note) { create(:discussion_note_on_issue, :system, noteable: issuable, project: project) }

        it_behaves_like 'issuable notes filter'
      end

1407 1408 1409
      context 'with cross-reference system note', :request_store do
        let(:new_issue) { create(:issue) }
        let(:cross_reference) { "mentioned in #{new_issue.to_reference(issue.project)}" }
1410

1411 1412 1413
        before do
          create(:discussion_note_on_issue, :system, noteable: issue, project: issue.project, note: cross_reference)
        end
1414

1415
        it 'filters notes that the user should not see' do
blackst0ne's avatar
blackst0ne committed
1416
          get :discussions, params: { namespace_id: project.namespace, project_id: project, id: issue.iid }
1417

1418
          expect(json_response.count).to eq(1)
1419
        end
1420

1421 1422
        it 'does not result in N+1 queries' do
          # Instantiate the controller variables to ensure QueryRecorder has an accurate base count
blackst0ne's avatar
blackst0ne committed
1423
          get :discussions, params: { namespace_id: project.namespace, project_id: project, id: issue.iid }
1424

1425 1426 1427
          RequestStore.clear!

          control_count = ActiveRecord::QueryRecorder.new do
blackst0ne's avatar
blackst0ne committed
1428
            get :discussions, params: { namespace_id: project.namespace, project_id: project, id: issue.iid }
1429 1430 1431 1432 1433 1434
          end.count

          RequestStore.clear!

          create_list(:discussion_note_on_issue, 2, :system, noteable: issue, project: issue.project, note: cross_reference)

blackst0ne's avatar
blackst0ne committed
1435
          expect { get :discussions, params: { namespace_id: project.namespace, project_id: project, id: issue.iid } }.not_to exceed_query_limit(control_count)
1436
        end
1437 1438
      end
    end
1439
  end
1440 1441 1442 1443

  context 'private project with token authentication' do
    let(:private_project) { create(:project, :private) }

1444
    it_behaves_like 'authenticates sessionless user', :index, :atom, ignore_incrementing: true do
1445 1446 1447 1448 1449 1450 1451
      before do
        default_params.merge!(project_id: private_project, namespace_id: private_project.namespace)

        private_project.add_maintainer(user)
      end
    end

1452
    it_behaves_like 'authenticates sessionless user', :calendar, :ics, ignore_incrementing: true do
1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475
      before do
        default_params.merge!(project_id: private_project, namespace_id: private_project.namespace)

        private_project.add_maintainer(user)
      end
    end
  end

  context 'public project with token authentication' do
    let(:public_project) { create(:project, :public) }

    it_behaves_like 'authenticates sessionless user', :index, :atom, public: true do
      before do
        default_params.merge!(project_id: public_project, namespace_id: public_project.namespace)
      end
    end

    it_behaves_like 'authenticates sessionless user', :calendar, :ics, public: true do
      before do
        default_params.merge!(project_id: public_project, namespace_id: public_project.namespace)
      end
    end
  end
1476
end