# frozen_string_literal: true

require 'spec_helper'

shared_examples 'instance security dashboard vulnerability findings endpoint' do
  let(:findings_headers) { { 'ACCEPT' => 'application/json' } }
  let(:findings_project) { create(:project) }
  let(:findings_user) { create(:user) }

  let(:fetcher_double) do
    double(
      findings_fetcher,
      execute: Vulnerabilities::Occurrence.none,
      findings_counter: {}
    )
  end

  before do
    findings_project.add_developer(findings_user)
    findings_user.security_dashboard_projects << findings_project

    login_as(findings_user)
    stub_licensed_features(security_dashboard: true)
  end

  context 'when no project ID param is given' do
    it "defaults to the current user's security dashboard projects" do
      _other_project = create(:project)

      expect(findings_fetcher).to receive(:new).with(
        anything,
        params: hash_including('project_id' => [findings_project.id])
      ).and_return(fetcher_double)

      get findings_endpoint, headers: findings_headers

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

  context 'when the request includes IDs from projects the current user does not have on their dashboard' do
    it "only fetches vulnerabilities from the current user's security dashboard projects" do
      inaccessible_project = create(:project)
      project_ids_param = { project_id: [findings_project.id, inaccessible_project.id] }

      expect(findings_fetcher).to receive(:new).with(
        anything,
        params: hash_including('project_id' => [findings_project.id])
      ).and_return(fetcher_double)

      get findings_endpoint, headers: findings_headers, params: project_ids_param

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

  context 'when the request only includes invalid project IDs' do
    it 'replies with an empty response' do
      inaccessible_project = create(:project)
      project_ids_param = { project_id: [inaccessible_project.id] }

      expect(findings_fetcher).not_to receive(:new)

      get findings_endpoint, headers: findings_headers, params: project_ids_param

      expect(response).to have_gitlab_http_status(:ok)
      expect(json_response).to be_empty
    end
  end

  context 'when the request includes project IDs the current user is not a member of' do
    context 'and the user is not an auditor' do
      it 'only fetches vulnerabilities from projects to which the user has access' do
        inaccessible_project = create(:project)
        findings_user.security_dashboard_projects << inaccessible_project
        project_ids_param = { project_id: [findings_project.id, inaccessible_project.id] }

        expect(findings_fetcher).to receive(:new).with(
          anything,
          params: hash_including('project_id' => [findings_project.id])
        ).and_return(fetcher_double)

        get findings_endpoint, headers: findings_headers, params: project_ids_param

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

    context 'and the user is an auditor' do
      let(:findings_user) { create(:auditor) }

      it 'fetches vulnerabilities for all given projects on their dashboard' do
        unmembered_project = create(:project)
        findings_user.security_dashboard_projects << unmembered_project
        project_ids_param = { project_id: [findings_project.id, unmembered_project.id] }

        expect(findings_fetcher).to receive(:new).with(
          anything,
          params: hash_including('project_id' => array_including(findings_project.id, unmembered_project.id))
        ).and_return(fetcher_double)

        get findings_endpoint, headers: findings_headers, params: project_ids_param

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

describe 'GET /-/security/vulnerability_findings' do
  it_behaves_like 'instance security dashboard JSON endpoint' do
    let(:security_dashboard_request) do
      get security_vulnerability_findings_path, headers: { 'ACCEPT' => 'application/json' }
    end
  end

  context 'when the current user is authenticated' do
    let(:findings_request_params) { project_ids_param }
    let(:headers) { { 'ACCEPT' => 'application/json' } }
    let(:pipeline) { create(:ci_pipeline, :success, project: project) }
    let(:project) { create(:project) }
    let(:project_ids_param) { { project_id: [project.id] } }
    let(:user) { create(:user) }

    it_behaves_like 'instance security dashboard vulnerability findings endpoint' do
      let(:findings_endpoint) { security_vulnerability_findings_path }
      let(:findings_fetcher) { ::Security::VulnerabilityFindingsFinder }
    end

    before do
      project.add_developer(user)
      user.security_dashboard_projects << project

      login_as(user)
      stub_licensed_features(security_dashboard: true)
    end

    subject { get security_vulnerability_findings_path, headers: headers, params: findings_request_params }

    it 'returns an ordered list of vulnerability findings for the given projects' do
      critical_vulnerability = create(
        :vulnerabilities_occurrence,
        pipelines: [pipeline],
        project: project,
        severity: :critical
      )
      create(:vulnerabilities_occurrence, pipelines: [pipeline], project: project, severity: :high)

      subject

      expect(response).to have_gitlab_http_status(200)
      expect(json_response.length).to eq 2
      expect(json_response.first['id']).to be(critical_vulnerability.id)
      expect(response).to match_response_schema('vulnerabilities/occurrence_list', dir: 'ee')
    end

    context 'when a specific page is requested' do
      let(:findings_request_params) { project_ids_param.merge(page: 2) }

      before do
        Vulnerabilities::Occurrence.paginates_per 2

        create_list(:vulnerabilities_occurrence, 3, pipelines: [pipeline], project: project)

        subject
      end

      after do
        Vulnerabilities::Occurrence.paginates_per Vulnerabilities::Occurrence::OCCURRENCES_PER_PAGE
      end

      it 'returns the list of vulnerability findings that are on the requested page' do
        expect(json_response.length).to eq 1
      end
    end

    context 'when the vulnerability findings have feedback' do
      before do
        vulnerability = create(:vulnerabilities_occurrence, pipelines: [pipeline], project: project, report_type: :sast)
        create(:vulnerability_feedback,
               :sast,
               :issue,
               pipeline: pipeline,
               issue: create(:issue, project: project),
               project: project,
               project_fingerprint: vulnerability.project_fingerprint)
      end

      it 'avoids N+1 queries', :with_request_store do
        control_count = ActiveRecord::QueryRecorder.new { subject }

        vulnerability = create(:vulnerabilities_occurrence, pipelines: [pipeline], project: project, report_type: :sast)
        create(:vulnerability_feedback,
               :sast,
               :issue,
               pipeline: pipeline,
               issue: create(:issue, project: project),
               project: project,
               project_fingerprint: vulnerability.project_fingerprint)

        expect { subject }.not_to exceed_all_query_limit(control_count)
      end
    end

    context 'with multiple report types' do
      before do
        create(:vulnerabilities_occurrence, pipelines: [pipeline], project: project, report_type: :sast)
        create(:vulnerabilities_occurrence, pipelines: [pipeline], project: project, report_type: :dast)
        create(:vulnerabilities_occurrence, pipelines: [pipeline], project: project, report_type: :dependency_scanning)

        subject
      end

      context 'with a single report filter' do
        let(:findings_request_params) { project_ids_param.merge(report_type: ['sast']) }

        it 'returns a list of vulnerability findings for that report type only' do
          expect(json_response.length).to eq 1
          expect(json_response.map { |v| v['report_type'] }.uniq).to contain_exactly('sast')
        end
      end

      context 'with multiple report filters' do
        let(:findings_request_params) { project_ids_param.merge(report_type: %w[sast dependency_scanning]) }

        it 'returns a list of vulnerability findings for all filtered upon types' do
          expect(json_response.length).to eq 2
          expect(json_response.map { |v| v['report_type'] }.uniq).to contain_exactly('sast', 'dependency_scanning')
        end
      end
    end
  end
end

describe 'GET /-/security/vulnerability_findings/summary' do
  it_behaves_like 'instance security dashboard JSON endpoint' do
    let(:security_dashboard_request) do
      get summary_security_vulnerability_findings_path, headers: { 'ACCEPT' => 'application/json' }
    end
  end

  context 'when the current user is authenticated' do
    let(:findings_request_params) { project_ids_param }
    let(:headers) { { 'ACCEPT' => 'application/json' } }
    let(:project_ids_param) { { project_id: [project.id] } }
    let(:pipeline) { create(:ci_pipeline, :success, project: project) }
    let(:user) { create(:user) }
    let(:project) { create(:project) }

    it_behaves_like 'instance security dashboard vulnerability findings endpoint' do
      let(:findings_endpoint) { summary_security_vulnerability_findings_path }
      let(:findings_fetcher) { ::Security::VulnerabilityFindingsFinder }
    end

    before do
      project.add_developer(user)
      user.security_dashboard_projects << project

      login_as(user)
      stub_licensed_features(security_dashboard: true)

      create_list(:vulnerabilities_occurrence, 3,
                  pipelines: [pipeline], project: project, report_type: :sast, severity: :high)
      create_list(:vulnerabilities_occurrence, 2,
                  pipelines: [pipeline], project: project, report_type: :dependency_scanning, severity: :low)
      create_list(:vulnerabilities_occurrence, 1,
                  pipelines: [pipeline], project: project, report_type: :dast, severity: :medium)
      create_list(:vulnerabilities_occurrence, 1,
                  pipelines: [pipeline], project: project, report_type: :sast, severity: :medium)
    end

    subject { get summary_security_vulnerability_findings_path, headers: headers, params: findings_request_params }

    it 'returns vulnerability findings counts for all report types' do
      subject

      expect(response).to have_gitlab_http_status(200)
      expect(json_response['high']).to eq(3)
      expect(json_response['low']).to eq(2)
      expect(json_response['medium']).to eq(2)
      expect(response).to match_response_schema('vulnerabilities/summary', dir: 'ee')
    end

    context 'with enabled filters' do
      let(:findings_request_params) { project_ids_param.merge(report_type: %w[sast dast], severity: %w[high low]) }

      it 'returns counts for filtered vulnerability findings' do
        subject

        expect(json_response['high']).to eq(3)
        expect(json_response['medium']).to eq(0)
        expect(json_response['low']).to eq(0)
      end
    end
  end
end

describe 'GET /-/security/vulnerability_findings/history' do
  it_behaves_like 'instance security dashboard JSON endpoint' do
    let(:security_dashboard_request) do
      get(
        history_security_vulnerability_findings_path,
        headers: { 'ACCEPT' => 'application/json' },
        params: { project_id: [] }
      )
    end
  end

  context 'when the current user is authenticated' do
    let(:findings_request_params) { project_ids_param }
    let(:headers) { { 'ACCEPT' => 'application/json' } }
    let(:pipeline) { create(:ci_pipeline, :success, project: project) }
    let(:project) { create(:project) }
    let(:project_ids_param) { { project_id: [project.id] } }
    let(:user) { create(:user) }

    it_behaves_like 'instance security dashboard vulnerability findings endpoint' do
      let(:findings_endpoint) { history_security_vulnerability_findings_path }
      let(:findings_fetcher) { ::Gitlab::Vulnerabilities::History }
    end

    before do
      project.add_developer(user)
      user.security_dashboard_projects << project

      login_as(user)
      stub_licensed_features(security_dashboard: true)

      travel_to(Time.zone.parse('2018-11-10')) do
        create(:vulnerabilities_occurrence,
               pipelines: [pipeline],
               project: project,
               report_type: :sast,
               severity: :critical)

        create(:vulnerabilities_occurrence,
               pipelines: [pipeline],
               project: project,
               report_type: :dependency_scanning,
               severity: :low)
      end

      travel_to(Time.zone.parse('2018-11-12')) do
        create(:vulnerabilities_occurrence,
               pipelines: [pipeline],
               project: project,
               report_type: :sast,
               severity: :critical)

        create(:vulnerabilities_occurrence,
               pipelines: [pipeline],
               project: project,
               report_type: :dependency_scanning,
               severity: :low)
      end
    end

    subject { get history_security_vulnerability_findings_path, headers: headers, params: findings_request_params }

    it 'returns vulnerability history within last 90 days' do
      travel_to(Time.zone.parse('2019-02-11')) do
        subject
      end

      expect(response).to have_gitlab_http_status(200)
      expect(json_response['total']).to eq({ '2018-11-12' => 2 })
      expect(json_response['critical']).to eq({ '2018-11-12' => 1 })
      expect(json_response['low']).to eq({ '2018-11-12' => 1 })
      expect(response).to match_response_schema('vulnerabilities/history', dir: 'ee')
    end

    it 'returns empty history if there are no vulnerabilities within last 90 days' do
      travel_to(Time.zone.parse('2019-02-13')) do
        subject
      end

      expect(json_response).to eq({
        "undefined" => {},
        "info" => {},
        "unknown" => {},
        "low" => {},
        "medium" => {},
        "high" => {},
        "critical" => {},
        "total" => {}
      })
    end

    context 'with a report type filter' do
      let(:findings_request_params) { project_ids_param.merge(report_type: %w[sast]) }

      before do
        travel_to(Time.zone.parse('2019-02-11')) do
          subject
        end
      end

      it 'returns filtered history if filters are enabled' do
        expect(json_response['total']).to eq({ '2018-11-12' => 1 })
        expect(json_response['critical']).to eq({ '2018-11-12' => 1 })
        expect(json_response['low']).to eq({})
      end
    end
  end
end
