updater/spec/github_api/dependency_submission_spec.rb
# typed: false
# frozen_string_literal: true

require "spec_helper"
require "support/dependency_file_helpers"

require "dependabot/bundler"
require "dependabot/dependency_file"
require "dependabot/dependency_snapshot"
require "dependabot/job"

require "github_api/dependency_submission"

RSpec.shared_examples "dependency_submission" do |empty|
  subject(:dependency_submission) do
    described_class.new(
      job_id: "9999",
      branch: branch,
      sha: sha,
      package_manager: "bundler",
      manifest_file: empty ? empty_file : lockfile,
      resolved_dependencies: empty ? {} : resolved_dependencies
    )
  end

  let(:branch) { "main" }
  let(:sha) { "fake-sha" }

  let(:directory) { "/" }

  let(:lockfile) do
    Dependabot::DependencyFile.new(
      name: "Gemfile.lock",
      content: fixture("bundler/original/Gemfile.lock"),
      directory: directory
    )
  end

  let(:empty_file) do
    Dependabot::DependencyFile.new(
      name: "",
      content: "",
      directory: directory
    )
  end

  let(:resolved_dependencies) do
    {
      "dummy-pkg-a" => Dependabot::DependencyGraphers::ResolvedDependency.new(
        package_url: "pkg:gem/dummy-pkg-a@2.0.0",
        direct: true,
        runtime: true,
        dependencies: []
      ),
      "dummy-pkg-b" => Dependabot::DependencyGraphers::ResolvedDependency.new(
        package_url: "pkg:gem/dummy-pkg-b@1.1.0",
        direct: false,
        runtime: false,
        dependencies: []
      )
    }
  end

  describe "::job_correlator" do
    [
      {
        context: "with a typical RubyGems project in directory root",
        directory: "/",
        expected_correlator: "dependabot-bundler"
      },
      {
        context: "with a RubyGems project in a subdirectory",
        directory: "ruby/backend-api/",
        expected_correlator: "dependabot-bundler-ruby-backend-api"
      },
      {
        context: "with mixed case in the file path",
        directory: "Ruby/backend-api/",
        expected_correlator: "dependabot-bundler-Ruby-backend-api"
      },
      # If we're given something pathologically long, we use a SHA256 to limit length
      {
        context: "with a RubyGems project in a pathological directory tree",
        directory: "lorem/ipsum/dolor/sit/amet/consectetur/adipiscing/elit/nunc/turpis/justo/" \
                   "maximus/ac/eleifend/sit/amet/malesuada/eu/nisi/donec/faucibus/lobortis/" \
                   "augue/vitae/venenatis/nunc/euismod/auctor/suspendisse/eget",
        expected_correlator: /dependabot-bundler-[a-fA-F0-9]{64}/
      }
    ].each do |tc|
      context tc[:context] do
        let(:directory) { tc[:directory] }

        it "uses the expected value for job.correlator" do
          payload = dependency_submission.payload

          expect(payload[:job][:correlator]).to match(tc[:expected_correlator])
        end
      end
    end
  end

  describe "payload" do
    it "generates submission metadata correctly" do
      payload = dependency_submission.payload

      # Check metadata
      expect(payload[:version]).to eq(described_class::SNAPSHOT_VERSION)
      expect(payload[:detector][:name]).to eq(described_class::SNAPSHOT_DETECTOR_NAME)
      expect(payload[:detector][:url]).to eq(described_class::SNAPSHOT_DETECTOR_URL)
      expect(payload[:detector][:version]).to eq(Dependabot::VERSION)
      expect(payload[:job][:correlator]).to eq("dependabot-bundler")
      expect(payload[:job][:id]).to eq("9999")
    end

    it "generates git attributes correctly" do
      payload = dependency_submission.payload

      expect(payload[:sha]).to eq(sha)
      expect(payload[:ref]).to eql("refs/heads/main")
    end

    context "when given a symbolic reference for the job's branch" do
      let(:branch) { "refs/heads/release" }

      it "does not add an additional refs/heads/ prefix" do
        payload = dependency_submission.payload

        expect(payload[:sha]).to eq(sha)
        expect(payload[:ref]).to eql("refs/heads/release")
      end
    end

    context "when given a symbolic reference for the job's branch with a leading /" do
      let(:branch) { "/refs/heads/release" }

      it "removes the leading slash" do
        payload = dependency_submission.payload

        expect(payload[:sha]).to eq(sha)
        expect(payload[:ref]).to eql("refs/heads/release")
      end
    end

    it "generates a valid manifest list" do
      payload = dependency_submission.payload

      if dependency_submission.resolved_dependencies.empty?
        expect(payload[:manifests]).to be_empty
        next
      end

      # We only expect a lockfile to be returned
      expect(payload[:manifests].length).to eq(1)

      # Lockfile data is correct
      lockfile = payload[:manifests].fetch("/Gemfile.lock")
      expect(lockfile[:name]).to eq("/Gemfile.lock")
      expect(lockfile[:file][:source_location]).to eq("Gemfile.lock")

      # Resolved dependencies are correct
      expect(lockfile[:resolved].length).to eq(2)

      dependency1 = lockfile[:resolved]["dummy-pkg-a"]
      expect(dependency1[:package_url]).to eql("pkg:gem/dummy-pkg-a@2.0.0")
      expect(dependency1[:relationship]).to eql("direct")
      expect(dependency1[:scope]).to eql("runtime")

      dependency2 = lockfile[:resolved]["dummy-pkg-b"]
      expect(dependency2[:package_url]).to eql("pkg:gem/dummy-pkg-b@1.1.0")
      expect(dependency2[:relationship]).to eql("indirect")
      expect(dependency2[:scope]).to eql("development")
    end
  end
end

RSpec.describe GithubApi::DependencySubmission do
  include DependencyFileHelpers

  context "with resolved dependencies" do
    it_behaves_like "dependency_submission", false
  end

  context "without resolved dependencies" do
    it_behaves_like "dependency_submission", true
  end
end
