bundler/spec/dependabot/bundler/update_checker_spec.rb
# typed: false
# frozen_string_literal: true

require "spec_helper"
require "shared_contexts"

require "dependabot/bundler/update_checker"
require "dependabot/dependency_file"
require "dependabot/dependency"
require "dependabot/requirements_update_strategy"
require_common_spec "update_checkers/shared_examples_for_update_checkers"

RSpec.describe Dependabot::Bundler::UpdateChecker do
  let(:rubygems_url) { "https://rubygems.org/api/v1/" }
  let(:requirements) do
    [{ file: "Gemfile", requirement: "~> 1.4.0", groups: [], source: nil }]
  end
  let(:current_version) { "1.4.0" }
  let(:dependency_name) { "business" }
  let(:dependency) do
    Dependabot::Dependency.new(
      name: dependency_name,
      version: current_version,
      requirements: requirements,
      package_manager: "bundler"
    )
  end
  let(:requirements_update_strategy) { nil }
  let(:security_advisories) { [] }
  let(:ignored_versions) { [] }
  let(:directory) { "/" }
  let(:github_token) { "token" }
  let(:dependency_files) { bundler_project_dependency_files("gemfile") }
  let(:update_cooldown) { nil }
  let(:credentials) do
    [{
      "type" => "git_source",
      "host" => "github.com",
      "username" => "x-access-token",
      "password" => "token"
    }]
  end
  let(:checker) do
    described_class.new(
      dependency: dependency,
      dependency_files: dependency_files,
      credentials: credentials,
      ignored_versions: ignored_versions,
      security_advisories: security_advisories,
      update_cooldown: update_cooldown,
      requirements_update_strategy: requirements_update_strategy
    )
  end

  before do
    stub_request(:get, /.*/).to_return(status: 404)
  end

  it_behaves_like "an update checker"

  describe "#latest_version" do
    subject { checker.latest_version }

    context "with a rubygems source" do
      before do
        rubygems_response = fixture("ruby", "rubygems_response_versions.json")
        stub_request(:get, rubygems_url + "versions/business.json")
          .to_return(status: 200, body: rubygems_response)
      end

      it { is_expected.to eq(Dependabot::Bundler::Version.new("1.5.0")) }

      context "when that only appears in the lockfile" do
        let(:dependency_files) { bundler_project_dependency_files("subdependency") }

        let(:requirements) { [] }
        let(:dependency_name) { "i18n" }
        let(:current_version) { "0.7.0.beta1" }

        before do
          rubygems_response = fixture("ruby", "rubygems_response_versions.json")
          stub_request(:get, rubygems_url + "versions/i18n.json")
            .to_return(status: 200, body: rubygems_response)
        end

        it { is_expected.to eq(Dependabot::Bundler::Version.new("1.6.0.beta")) }
      end

      context "when the gem isn't on Rubygems" do
        before do
          stub_request(:get, rubygems_url + "versions/business.json")
            .to_return(status: 404, body: "This rubygem could not be found.")
        end

        it { is_expected.to be_nil }
      end

      context "with a Gemfile that includes a file with require_relative" do
        let(:dependency_files) { bundler_project_dependency_files("includes_require_relative_gemfile") }
        let(:directory) { "app/" }

        it { is_expected.to eq(Dependabot::Bundler::Version.new("1.5.0")) }
      end

      context "with a gem.rb and gems.locked setup" do
        let(:dependency_files) { bundler_project_dependency_files("gems_rb") }

        let(:requirements) do
          [{
            file: "gems.rb",
            requirement: "~> 1.4.0",
            groups: [],
            source: nil
          }]
        end

        it { is_expected.to eq(Dependabot::Bundler::Version.new("1.5.0")) }
      end
    end

    context "with extra nonrelevant credentials" do
      before do
        rubygems_response = fixture("ruby", "rubygems_response_versions.json")
        stub_request(:get, rubygems_url + "versions/business.json")
          .to_return(status: 200, body: rubygems_response)
      end

      let(:credentials) do
        [{
          "type" => "git_source",
          "host" => "github.com",
          "username" => "x-access-token",
          "password" => "token"
        }, {
          "type" => "npm_registry",
          "registry" => "npm.fury.io/dependabot",
          "token" => "secret_token"
        }]
      end

      it { is_expected.to eq(Dependabot::Bundler::Version.new("1.5.0")) }
    end

    context "with a private rubygems source" do
      let(:dependency_files) { bundler_project_dependency_files("specified_source") }

      let(:requirements) do
        [{
          file: "Gemfile",
          requirement: ">= 0",
          groups: [],
          source: { type: "rubygems" }
        }]
      end
      let(:registry_url) { "https://repo.fury.io/greysteil/" }
      let(:gemfury_business_url) do
        "https://repo.fury.io/greysteil/api/v1/dependencies?gems=business"
      end

      before do
        bundler_version = PackageManagerHelper.bundler_version

        # We only need to stub out the version callout since it would
        # otherwise call out to the internet in a shell command
        allow(Dependabot::Bundler::NativeHelpers)
          .to receive(:run_bundler_subprocess)
          .with({
            bundler_version: bundler_version,
            function: "dependency_source_type",
            options: anything,
            args: anything
          }).and_return("private")

        allow(Dependabot::Bundler::NativeHelpers)
          .to receive(:run_bundler_subprocess)
          .with({
            bundler_version: bundler_version,
            function: "private_registry_versions",
            options: anything,
            args: anything
          })
          .and_return(
            ["1.5.0", "1.9.0", "1.10.0.beta"]
          )

        rubygems_response = fixture("ruby", "rubygems_response_versions.json")
        stub_request(:get, rubygems_url + "versions/business.json")
          .to_return(status: 200, body: rubygems_response)
      end

      it { is_expected.to eq(Dependabot::Bundler::Version.new("1.5.0")) }
    end

    context "when given a git source" do
      let(:dependency_files) { bundler_project_dependency_files("git_source_no_ref") }

      before do
        rubygems_response = fixture("ruby", "rubygems_response_versions.json")
        stub_request(:get, rubygems_url + "versions/business.json")
          .to_return(status: 200, body: rubygems_response)
        stub_request(:get, "https://github.com/dependabot-fixtures/business/api/v1/versions/business.json")
          .to_return(status: 200, body: rubygems_response)
      end

      context "when that is the gem we're checking for" do
        let(:dependency_name) { "business" }
        let(:current_version) { "a1b78a929dac93a52f08db4f2847d76d6cfe39bd" }
        let(:requirements) do
          [{
            file: "Gemfile",
            requirement: ">= 0",
            groups: [],
            source: {
              type: "git",
              url: "https://github.com/dependabot-fixtures/business",
              branch: "master",
              ref: "master"
            }
          }]
        end

        context "when head of the gem's branch is included in a release" do
          before do
            allow_any_instance_of(Dependabot::GitCommitChecker)
              .to receive(:branch_or_ref_in_release?)
              .and_return(true)
          end

          it { is_expected.to eq("799cd60c8cd51c9f379b423d506fc02c3a68e895") }
        end

        context "when head of the gem's branch is not included in a release" do
          before do
            allow_any_instance_of(Dependabot::GitCommitChecker)
              .to receive(:branch_or_ref_in_release?)
              .and_return(false)
            git_url = "https://github.com/dependabot-fixtures/business.git"
            git_header = {
              "content-type" => "application/x-git-upload-pack-advertisement"
            }
            stub_request(:get, git_url + "/info/refs?service=git-upload-pack")
              .to_return(
                status: 200,
                body: fixture("git", "upload_packs", "business"),
                headers: git_header
              )
          end

          it "fetches the latest SHA-1 hash" do
            expect(checker.latest_version)
              .to eq("7bb4e41ce5164074a0920d5b5770d196b4d90104")
          end
        end

        context "when the gem's tag is pinned" do
          let(:dependency_files) { bundler_project_dependency_files("git_source") }

          let(:requirements) do
            [{
              file: "Gemfile",
              requirement: ">= 0",
              groups: [],
              source: {
                type: "git",
                url: "https://github.com/dependabot-fixtures/business",
                branch: "master",
                ref: "a1b78a9"
              }
            }]
          end

          context "when the gem isn't on Rubygems" do
            before do
              stub_request(:get, rubygems_url + "gems/business.json")
                .to_return(status: 404, body: "This rubygem could not be found.")
            end

            it { is_expected.to eq(current_version) }
          end

          context "when the reference isn't included in the new version" do
            before do
              allow_any_instance_of(Dependabot::GitCommitChecker)
                .to receive(:branch_or_ref_in_release?)
                .and_return(false)
            end

            it "respects the pin" do
              expect(checker.latest_version).to eq(current_version)
              expect(checker.can_update?(requirements_to_unlock: :own))
                .to be(false)
            end
          end

          context "when the reference is included in the new version" do
            before do
              allow_any_instance_of(Dependabot::GitCommitChecker)
                .to receive(:branch_or_ref_in_release?)
                .and_return(true)
            end

            it { is_expected.to eq("a1b78a929dac93a52f08db4f2847d76d6cfe39bd") }
          end

          context "when the pin looks like a version" do
            let(:requirements) do
              [{
                file: "Gemfile",
                requirement: ">= 0",
                groups: [],
                source: {
                  type: "git",
                  url: "https://github.com/dependabot-fixtures/business",
                  branch: "master",
                  ref: "v1.0.0"
                }
              }]
            end
            let(:upload_pack_fixture) { "business" }

            before do
              stub_request(:get, rubygems_url + "gems/business.json")
                .to_return(status: 404, body: "This rubygem could not be found.")
              url = "https://github.com/dependabot-fixtures/business.git"
              git_header = {
                "content-type" => "application/x-git-upload-pack-advertisement"
              }
              stub_request(:get, url + "/info/refs?service=git-upload-pack")
                .with(basic_auth: %w(x-access-token token))
                .to_return(
                  status: 200,
                  body: fixture("git", "upload_packs", upload_pack_fixture),
                  headers: git_header
                )
            end

            it "fetches the latest SHA-1 hash of the latest version tag" do
              expect(checker.latest_version)
                .to eq("37f41032a0f191507903ebbae8a5c0cb945d7585")
            end

            context "when there are no tags" do
              let(:upload_pack_fixture) { "no_tags" }

              it "returns the current version" do
                expect(checker.latest_version).to eq(current_version)
              end
            end
          end
        end
      end
    end

    context "when given a path source" do
      let(:dependency_files) { bundler_project_dependency_files("path_source") }

      before do
        rubygems_response = fixture("ruby", "rubygems_response_versions.json")
        stub_request(:get, rubygems_url + "versions/example.json")
          .to_return(status: 200, body: rubygems_response)
      end

      context "with a downloaded gemspec" do
        context "when that is the gem we're checking" do
          let(:dependency_name) { "example" }
          let(:current_version) { "0.9.3" }
          let(:requirements) do
            [{
              file: "Gemfile",
              requirement: ">= 0",
              groups: [],
              source: { type: "path" }
            }]
          end

          it { is_expected.to be_nil }
        end
      end
    end

    describe "with cooldown options" do
      let(:update_cooldown) do
        Dependabot::Package::ReleaseCooldownOptions.new(default_days: 7)
      end
      let(:expected_cooldown_options) do
        Dependabot::Package::ReleaseCooldownOptions.new(
          default_days: 7,
          semver_major_days: 7,
          semver_minor_days: 7,
          semver_patch_days: 7,
          include: [],
          exclude: []
        )
      end

      before do
        # Mock the LatestVersionFinder to verify it receives cooldown_options
        latest_version_finder = instance_double(Dependabot::Bundler::UpdateChecker::LatestVersionFinder)
        allow(latest_version_finder)
          .to receive(:latest_version_details).and_return({ version: Dependabot::Bundler::Version.new("1.5.0") })
        allow(Dependabot::Bundler::UpdateChecker::LatestVersionFinder)
          .to receive(:new).and_return(latest_version_finder)
      end

      it "passes cooldown_options to LatestVersionFinder" do
        checker.latest_version

        expect(Dependabot::Bundler::UpdateChecker::LatestVersionFinder).to have_received(:new).with(
          hash_including(
            cooldown_options: an_object_having_attributes(
              default_days: expected_cooldown_options.default_days,
              semver_major_days: expected_cooldown_options.semver_major_days,
              semver_minor_days: expected_cooldown_options.semver_minor_days,
              semver_patch_days: expected_cooldown_options.semver_patch_days,
              include: expected_cooldown_options.include,
              exclude: expected_cooldown_options.exclude
            )
          )
        )
      end
    end
  end

  describe "#lowest_security_fix_version" do
    subject(:lowest_security_fix_version) { checker.lowest_security_fix_version }

    context "with a rubygems source" do
      let(:current_version) { "1.2.0" }
      let(:requirements) do
        [{ file: "Gemfile", requirement: "~> 1.2.0", groups: [], source: nil }]
      end

      before do
        rubygems_response = fixture("ruby", "rubygems_response_versions.json")
        stub_request(:get, rubygems_url + "versions/business.json")
          .to_return(status: 200, body: rubygems_response)
      end

      it "finds the lowest available non-vulnerable version" do
        expect(lowest_security_fix_version).to eq(Dependabot::Bundler::Version.new("1.3.0"))
      end

      context "with a security vulnerability" do
        let(:security_advisories) do
          [
            Dependabot::SecurityAdvisory.new(
              dependency_name: dependency_name,
              package_manager: "bundler",
              vulnerable_versions: ["<= 1.3.0"]
            )
          ]
        end

        it "finds the lowest available non-vulnerable version" do
          expect(lowest_security_fix_version).to eq(Dependabot::Bundler::Version.new("1.4.0"))
        end
      end
    end
  end

  describe "#latest_version_resolvable_with_full_unlock?" do
    subject { checker.send(:latest_version_resolvable_with_full_unlock?) }

    include_context "when stubbing rubygems compact index"

    context "with no latest version" do
      before do
        stub_request(:get, rubygems_url + "versions/business.json")
          .to_return(status: 404, body: "This rubygem could not be found.")
      end

      it { is_expected.to be_falsey }
    end

    context "with a latest version" do
      before do
        allow(checker).to receive(:latest_version).and_return(Dependabot::Bundler::Version.new(target_version))
      end

      context "when the force updater raises" do
        let(:dependency_files) { bundler_project_dependency_files("subdep_blocked_by_subdep") }
        let(:target_version) { "2.0.0" }
        let(:dependency_name) { "dummy-pkg-a" }
        let(:requirements) do
          [{
            file: "Gemfile",
            requirement: "~> 1.0.0",
            groups: [],
            source: nil
          }]
        end

        it { is_expected.to be_falsey }
      end

      context "when the force updater succeeds" do
        let(:dependency_files) { bundler_project_dependency_files("version_conflict") }
        let(:target_version) { "3.6.0" }
        let(:dependency_name) { "rspec-mocks" }
        let(:requirements) do
          [{ file: "Gemfile", requirement: "3.5.0", groups: [], source: nil }]
        end

        it { is_expected.to be_truthy }
      end
    end
  end

  describe "#updated_dependencies_after_full_unlock" do
    subject(:updated_dependencies_after_full_unlock) do
      checker.send(:updated_dependencies_after_full_unlock)
    end

    include_context "when stubbing rubygems compact index"

    context "with a latest version" do
      before do
        allow(checker).to receive(:latest_version).and_return(Dependabot::Bundler::Version.new(target_version))
      end

      context "when the force updater succeeds" do
        let(:dependency_files) { bundler_project_dependency_files("version_conflict") }
        let(:target_version) { "3.6.0" }
        let(:dependency_name) { "rspec-mocks" }
        let(:requirements) do
          [{
            file: "Gemfile",
            requirement: "3.5.0",
            groups: [:default],
            source: nil
          }]
        end
        let(:expected_requirements) do
          [{
            file: "Gemfile",
            requirement: "3.6.0",
            groups: [:default],
            source: nil
          }]
        end

        it "returns the right array of updated dependencies" do
          expect(updated_dependencies_after_full_unlock).to contain_exactly(
            Dependabot::Dependency.new(
              name: "rspec-mocks",
              version: "3.6.0",
              previous_version: "3.5.0",
              requirements: expected_requirements,
              previous_requirements: requirements,
              package_manager: "bundler"
            ),
            Dependabot::Dependency.new(
              name: "rspec-support",
              version: "3.6.0",
              previous_version: "3.5.0",
              requirements: expected_requirements,
              previous_requirements: requirements,
              package_manager: "bundler"
            )
          )
        end

        context "with a gem.rb and gems.locked setup" do
          let(:dependency_files) { bundler_project_dependency_files("version_conflict_gems_rb") }

          let(:requirements) do
            [{
              file: "gems.rb",
              requirement: "3.5.0",
              groups: [:default],
              source: nil
            }]
          end
          let(:expected_requirements) do
            [{
              file: "gems.rb",
              requirement: "3.6.0",
              groups: [:default],
              source: nil
            }]
          end

          it "returns the right array of updated dependencies" do
            expect(updated_dependencies_after_full_unlock).to contain_exactly(
              Dependabot::Dependency.new(
                name: "rspec-mocks",
                version: "3.6.0",
                previous_version: "3.5.0",
                requirements: expected_requirements,
                previous_requirements: requirements,
                package_manager: "bundler"
              ),
              Dependabot::Dependency.new(
                name: "rspec-support",
                version: "3.6.0",
                previous_version: "3.5.0",
                requirements: expected_requirements,
                previous_requirements: requirements,
                package_manager: "bundler"
              )
            )
          end
        end
      end
    end
  end

  describe "#conflicting_dependencies" do
    subject(:conflicting_dependencies) { checker.conflicting_dependencies }

    include_context "when stubbing rubygems compact index"
    include_context "when stubbing rubygems versions api"

    let(:dependency_files) { bundler_project_dependency_files("subdep_blocked_by_subdep") }
    let(:target_version) { "2.0.0" }
    let(:dependency_name) { "dummy-pkg-a" }
    let(:requirements) do
      [{
        file: "Gemfile",
        requirement: "~> 1.0.0",
        groups: [],
        source: nil
      }]
    end

    let(:requirements) { [] }
    let(:security_advisories) do
      [
        Dependabot::SecurityAdvisory.new(
          dependency_name: dependency_name,
          package_manager: "bundler",
          vulnerable_versions: ["< 2.0.0"]
        )
      ]
    end

    before do
      allow(checker)
        .to receive(:lowest_security_fix_version)
        .and_return(target_version)
    end

    it do
      expect(conflicting_dependencies).to eq(
        [{
          "explanation" => "dummy-pkg-b (1.0.0) requires dummy-pkg-a (< 2.0.0)",
          "name" => "dummy-pkg-b",
          "version" => "1.0.0",
          "requirement" => "< 2.0.0"
        }]
      )
    end

    context "when lowest_security_fix_version returns a Version object" do
      let(:version_object) { Dependabot::Bundler::Version.new("2.0.0") }
      let(:mock_resolver) { instance_double(Dependabot::Bundler::UpdateChecker::ConflictingDependencyResolver) }

      before do
        allow(checker)
          .to receive(:lowest_security_fix_version)
          .and_return(version_object)

        # Mock the ConflictingDependencyResolver constructor to return our mock
        allow(Dependabot::Bundler::UpdateChecker::ConflictingDependencyResolver)
          .to receive(:new)
          .and_return(mock_resolver)

        # Set up the mock to expect the string version and return a result
        allow(mock_resolver)
          .to receive(:conflicting_dependencies)
          .with(
            dependency: dependency,
            target_version: "2.0.0" # Should be a string, not Version object
          )
          .and_return([
            {
              "explanation" => "dummy-pkg-b (1.0.0) requires dummy-pkg-a (< 2.0.0)",
              "name" => "dummy-pkg-b",
              "version" => "1.0.0",
              "requirement" => "< 2.0.0"
            }
          ])
      end

      it "converts the Version object to string before passing to ConflictingDependencyResolver" do
        result = conflicting_dependencies

        expect(result).to eq(
          [
            {
              "explanation" => "dummy-pkg-b (1.0.0) requires dummy-pkg-a (< 2.0.0)",
              "name" => "dummy-pkg-b",
              "version" => "1.0.0",
              "requirement" => "< 2.0.0"
            }
          ]
        )

        # Verify that the ConflictingDependencyResolver was called with a string target_version
        expect(mock_resolver)
          .to have_received(:conflicting_dependencies)
          .with(
            dependency: dependency,
            target_version: "2.0.0"
          )
      end

      it "does not raise a Sorbet type error when converting Version to String" do
        expect { conflicting_dependencies }.not_to raise_error
      end

      it "preserves the version semantics when converting to string" do
        conflicting_dependencies

        # Verify the exact string representation matches what .to_s produces
        expect(mock_resolver)
          .to have_received(:conflicting_dependencies)
          .with(
            dependency: dependency,
            target_version: version_object.to_s
          )
      end
    end
  end

  describe "#latest_resolvable_version" do
    subject(:latest_resolvable_version) { checker.latest_resolvable_version }

    include_context "when stubbing rubygems compact index"
    include_context "when stubbing rubygems versions api"

    context "when given a gem from rubygems" do
      context "when that only appears in the lockfile" do
        let(:dependency_files) { bundler_project_dependency_files("subdependency") }
        let(:requirements) { [] }
        let(:dependency_name) { "i18n" }
        let(:current_version) { "0.7.0.beta1" }

        it { is_expected.to eq(Dependabot::Bundler::Version.new("0.7.0")) }
      end

      context "with no version specified" do
        let(:dependency_files) { bundler_project_dependency_files("version_not_specified") }
        let(:requirements) do
          [{ file: "Gemfile", requirement: ">= 0", groups: [], source: nil }]
        end

        it { is_expected.to eq(Dependabot::Bundler::Version.new("1.13.0")) }

        context "when the user is ignoring the latest version" do
          let(:ignored_versions) { [">= 1.7.0.a, < 2.0"] }

          it { is_expected.to eq(Dependabot::Bundler::Version.new("1.6.0")) }
        end
      end

      context "with a greater than or equal to matcher" do
        let(:dependency_files) { bundler_project_dependency_files("gte_matcher") }
        let(:requirements) do
          [{
            file: "Gemfile",
            requirement: ">= 1.4.0",
            groups: [],
            source: nil
          }]
        end

        it { is_expected.to eq(Dependabot::Bundler::Version.new("1.13.0")) }
      end

      context "with multiple requirements" do
        let(:dependency_files) { bundler_project_dependency_files("version_between_bounds_gemfile") }
        let(:requirements) do
          [{
            file: "Gemfile",
            requirement: "> 1.0.0, < 1.5.0",
            groups: [],
            source: nil
          }]
        end

        it { is_expected.to eq(Dependabot::Bundler::Version.new("1.13.0")) }
      end

      context "with a gem.rb and gems.locked setup" do
        context "when that only appears in the lockfile" do
          let(:dependency_files) { bundler_project_dependency_files("subdependency_gems_rb") }
          let(:requirements) { [] }
          let(:dependency_name) { "i18n" }
          let(:current_version) { "0.7.0.beta1" }

          it { is_expected.to eq(Dependabot::Bundler::Version.new("0.7.0")) }
        end

        context "with a range requirement" do
          let(:dependency_files) { bundler_project_dependency_files("version_between_bounds_gems_rb") }
          let(:requirements) do
            [{
              file: "gems.rb",
              requirement: "> 1.0.0, < 1.5.0",
              groups: [],
              source: nil
            }]
          end

          it { is_expected.to eq(Dependabot::Bundler::Version.new("1.13.0")) }
        end
      end
    end

    context "when given a gem with a path source" do
      context "with a downloaded gemspec" do
        let(:dependency_files) { bundler_project_dependency_files("path_source_no_overlap") }

        it { is_expected.to eq(Dependabot::Bundler::Version.new("1.13.0")) }

        it "doesn't persist any temporary changes to Bundler's root" do
          expect { checker.latest_resolvable_version }
            .not_to(change(::Bundler, :root))
        end

        context "when that requires other files" do
          let(:dependency_files) { bundler_project_dependency_files("path_source_no_overlap_with_require") }

          it { is_expected.to eq(Dependabot::Bundler::Version.new("1.13.0")) }
        end

        context "when that is the gem we're checking" do
          before do
            rubygems_response = fixture("ruby", "rubygems_response_versions.json")
            stub_request(:get, rubygems_url + "versions/example.json").to_return(status: 200, body: rubygems_response)
          end

          let(:dependency_name) { "example" }
          let(:current_version) { "0.9.3" }

          it { is_expected.to eq(Dependabot::Bundler::Version.new("0.9.3")) }
        end

        context "when that has a .specification" do
          let(:dependency_files) { bundler_project_dependency_files("path_source_statesman") }

          it { is_expected.to eq(Dependabot::Bundler::Version.new("1.13.0")) }
        end
      end
    end

    context "when given a gem with a git source" do
      let(:dependency_files) { bundler_project_dependency_files("git_source_no_ref") }

      context "when the gem under consideration is the one we're checking" do
        let(:dependency_name) { "business" }
        let(:current_version) { "cff701b3bfb182afc99a85657d7c9f3d6c1ccce2" }
        let(:requirements) do
          [{
            file: "Gemfile",
            requirement: ">= 0",
            groups: [],
            source: {
              type: "git",
              url: "https://github.com/dependabot-fixtures/business",
              branch: "master",
              ref: "master"
            }
          }]
        end

        context "when the head of the branch isn't released" do
          before do
            allow_any_instance_of(Dependabot::GitCommitChecker)
              .to receive(:branch_or_ref_in_release?)
              .and_return(false)
            git_url = "https://github.com/dependabot-fixtures/business.git"
            git_header = {
              "content-type" => "application/x-git-upload-pack-advertisement"
            }
            stub_request(:get, git_url + "/info/refs?service=git-upload-pack")
              .to_return(
                status: 200,
                body: fixture("git", "upload_packs", "business"),
                headers: git_header
              )
          end

          it "fetches the latest SHA-1 hash" do
            version = checker.latest_resolvable_version
            expect(version).to match(/^[0-9a-f]{40}$/)
            expect(version).not_to eq(current_version)
          end

          context "when the Gemfile doesn't specify a git source" do
            let(:dependency_files) { bundler_project_dependency_files("git_source_mismatched") }

            # If the dependency has a git version in the Gemfile.lock but not in
            # the Gemfile (i.e., because they're out-of-sync) we leave that
            # problem to the user.
            it { is_expected.to be_nil }
          end
        end

        context "when the head of the branch is released" do
          before do
            allow_any_instance_of(Dependabot::GitCommitChecker)
              .to receive(:branch_or_ref_in_release?)
              .and_return(true)
          end

          it { is_expected.to eq("799cd60c8cd51c9f379b423d506fc02c3a68e895") }
        end

        context "when the dependency has never been released" do
          let(:dependency_files) { bundler_project_dependency_files("git_source") }
          let(:current_version) { "cff701b3bfb182afc99a85657d7c9f3d6c1ccce2" }
          let(:requirements) do
            [{
              file: "Gemfile",
              requirement: ">= 0",
              groups: [],
              source: {
                type: "git",
                url: "https://github.com/dependabot-fixtures/prius",
                branch: "master",
                ref: "master"
              }
            }]
          end

          before do
            allow_any_instance_of(Dependabot::GitCommitChecker)
              .to receive(:branch_or_ref_in_release?)
              .and_return(false)
            git_url = "https://github.com/dependabot-fixtures/prius.git"
            git_header = {
              "content-type" => "application/x-git-upload-pack-advertisement"
            }
            stub_request(:get, git_url + "/info/refs?service=git-upload-pack")
              .to_return(
                status: 200,
                body: fixture("git", "upload_packs", "prius"),
                headers: git_header
              )
          end

          it "fetches the latest SHA-1 hash" do
            version = checker.latest_resolvable_version
            expect(version).to match(/^[0-9a-f]{40}$/)
            expect(version).not_to eq(current_version)
          end
        end

        context "when the gem's tag is pinned" do
          let(:dependency_files) { bundler_project_dependency_files("git_source") }
          let(:dependency_name) { "business" }
          let(:current_version) { "a1b78a929dac93a52f08db4f2847d76d6cfe39bd" }

          let(:requirements) do
            [{
              file: "Gemfile",
              requirement: ">= 0",
              groups: [],
              source: {
                type: "git",
                url: "https://github.com/dependabot-fixtures/business",
                branch: "master",
                ref: "a1b78a9"
              }
            }]
          end

          context "when the reference isn't included in the new version" do
            before do
              allow_any_instance_of(Dependabot::GitCommitChecker)
                .to receive(:branch_or_ref_in_release?)
                .and_return(false)
            end

            it "respects the pin" do
              expect(checker.latest_resolvable_version)
                .to eq("a1b78a929dac93a52f08db4f2847d76d6cfe39bd")
              expect(checker.can_update?(requirements_to_unlock: :own))
                .to be(false)
            end
          end

          context "when the reference is included in the new version" do
            before do
              allow_any_instance_of(Dependabot::GitCommitChecker)
                .to receive(:branch_or_ref_in_release?)
                .and_return(true)
            end

            it { is_expected.to eq("a1b78a929dac93a52f08db4f2847d76d6cfe39bd") }
          end

          context "when the release appears to be a version" do
            let(:requirements) do
              [{
                file: "Gemfile",
                requirement: ">= 0",
                groups: [],
                source: {
                  type: "git",
                  url: "https://github.com/dependabot-fixtures/business",
                  branch: "master",
                  ref: "v1.0.0"
                }
              }]
            end
            let(:upload_pack_fixture) { "business" }

            before do
              stub_request(:get, rubygems_url + "gems/business.json")
                .to_return(status: 404, body: "This rubygem could not be found.")
              url = "https://github.com/dependabot-fixtures/business.git"
              git_header = {
                "content-type" => "application/x-git-upload-pack-advertisement"
              }
              stub_request(:get, url + "/info/refs?service=git-upload-pack")
                .with(basic_auth: %w(x-access-token token))
                .to_return(
                  status: 200,
                  body: fixture("git", "upload_packs", upload_pack_fixture),
                  headers: git_header
                )
            end

            it "fetches the latest SHA-1 hash of the latest version tag" do
              expect(checker.latest_resolvable_version)
                .to eq("37f41032a0f191507903ebbae8a5c0cb945d7585")
            end

            context "when the dependency has never been released" do
              let(:dependency_files) { bundler_project_dependency_files("git_source_unreleased") }
              let(:dependency_name) { "dummy-git-dependency" }
              let(:current_version) do
                "20151f9b67c8a04461fa0ee28385b6187b86587b"
              end
              let(:upload_pack_fixture) { "dummy-git-dependency" }
              let(:requirements) do
                [{
                  file: "Gemfile",
                  requirement: ">= 0",
                  groups: [],
                  source: {
                    type: "git",
                    url: "https://github.com/dependabot-fixtures/" \
                         "ruby-dummy-git-dependency",
                    branch: nil,
                    ref: "v1.0.0"
                  }
                }]
              end

              before do
                stub_request(
                  :get, rubygems_url + "gems/dummy-git-dependency.json"
                ).to_return(status: 404)
                url = "https://github.com/dependabot-fixtures/" \
                      "ruby-dummy-git-dependency.git"
                git_header = {
                  "content-type" =>
                    "application/x-git-upload-pack-advertisement"
                }
                stub_request(:get, url + "/info/refs?service=git-upload-pack")
                  .with(basic_auth: %w(x-access-token token))
                  .to_return(
                    status: 200,
                    body: fixture("git", "upload_packs", upload_pack_fixture),
                    headers: git_header
                  )
              end

              it "returns the commit SHA for the updated version" do
                expect(checker.latest_resolvable_version)
                  .to eq("c0e25c2eb332122873f73acb3b61fb2e261cfd8f")
              end
            end

            context "when there are no tags" do
              let(:upload_pack_fixture) { "no_tags" }

              it "returns the current version" do
                expect(checker.latest_resolvable_version).to eq(current_version)
              end
            end

            context "when updating the gem results in a conflict" do
              let(:dependency_files) { bundler_project_dependency_files("git_source_with_tag_conflict") }
              let(:dependency_name) { "onfido" }
              let(:current_version) do
                "7b36eac82a7e42049052a58af0a7943fe0363714"
              end
              let(:requirements) do
                [{
                  file: "Gemfile",
                  requirement: ">= 0",
                  groups: [],
                  source: {
                    type: "git",
                    url: "https://github.com/hvssle/onfido",
                    branch: "master",
                    ref: "v0.4.0"
                  }
                }]
              end

              before do
                allow_any_instance_of(Dependabot::GitCommitChecker)
                  .to receive(:branch_or_ref_in_release?)
                  .and_return(false)
                refs_url = "https://github.com/hvssle/onfido.git/info/refs"
                git_header = {
                  "content-type" =>
                    "application/x-git-upload-pack-advertisement"
                }
                stub_request(:get, refs_url + "?service=git-upload-pack")
                  .to_return(
                    status: 200,
                    body: fixture("git", "upload_packs", "onfido"),
                    headers: git_header
                  )
              end

              it { is_expected.to eq(dependency.version) }
            end
          end
        end

        context "when the gem has a version specified, too" do
          let(:dependency_files) { bundler_project_dependency_files("git_source_with_version_gemfile") }

          let(:requirements) do
            [{
              file: "Gemfile",
              requirement: "~> 1.0.0",
              groups: [],
              source: {
                type: "git",
                url: "https://github.com/dependabot-fixtures/" \
                     "dependabot-test-ruby-package",
                branch: "master",
                ref: "master"
              }
            }]
          end
          let(:dependency_name) { "dependabot-test-ruby-package" }
          let(:current_version) { "81073f9462f228c6894e3e384d0718def310d99f" }

          before do
            allow_any_instance_of(Dependabot::GitCommitChecker)
              .to receive(:branch_or_ref_in_release?)
              .and_return(false)
            stub_request(
              :get, rubygems_url + "versions/dependabot-test-ruby-package.json"
            ).to_return(status: 404)
            git_url = "https://github.com/dependabot-fixtures/" \
                      "dependabot-test-ruby-package.git"
            git_header = {
              "content-type" => "application/x-git-upload-pack-advertisement"
            }
            stub_request(:get, git_url + "/info/refs?service=git-upload-pack")
              .to_return(
                status: 200,
                body: fixture(
                  "git",
                  "upload_packs",
                  "dependabot-test-ruby-package"
                ),
                headers: git_header
              )
          end

          it "fetches the latest SHA-1 hash" do
            version = checker.latest_resolvable_version
            expect(version).to match(/^[0-9a-f]{40}$/)
            expect(version).not_to eq "c5bf1bd47935504072ac0eba1006cf4d67af6a7a"
          end
        end

        context "when the gem has a bad branch" do
          let(:dependency_files) { bundler_project_dependency_files("bad_branch") }

          let(:dependency_name) { "prius" }
          let(:current_version) { "2.0.0" }
          let(:requirements) do
            [{
              file: "Gemfile",
              requirement: "~> 1.0.0",
              groups: [],
              source: {
                type: "git",
                url: "https://github.com/dependabot-fixtures/prius",
                branch: "master",
                ref: "master"
              }
            }]
          end

          before do
            allow_any_instance_of(Dependabot::GitCommitChecker)
              .to receive(:branch_or_ref_in_release?)
              .and_return(false)
            git_url = "https://github.com/dependabot-fixtures/prius.git"
            git_header = {
              "content-type" => "application/x-git-upload-pack-advertisement"
            }
            stub_request(:get, git_url + "/info/refs?service=git-upload-pack")
              .to_return(
                status: 200,
                body: fixture("git", "upload_packs", "prius"),
                headers: git_header
              )
            allow(checker)
              .to receive(:latest_resolvable_version_details)
              .and_call_original
            allow(checker)
              .to receive(:latest_resolvable_version_details)
              .with(remove_git_source: true)
              .and_return(version: Dependabot::Bundler::Version.new("2.0.0"))
          end

          it "raises a helpful error" do
            expect { checker.latest_resolvable_version }
              .to raise_error do |error|
                expect(error).to be_a Dependabot::GitDependencyReferenceNotFound
                expect(error.dependency).to eq("prius")
              end
          end
        end

        context "when updating the gem results in a conflict" do
          let(:dependency_files) { bundler_project_dependency_files("git_source_with_conflict") }
          let(:dependency_name) { "onfido" }
          let(:current_version) { "1.13.0" }
          let(:requirements) do
            [{
              file: "Gemfile",
              requirement: ">= 0",
              groups: [],
              source: {
                type: "git",
                url: "https://github.com/hvssle/onfido",
                branch: "master",
                ref: "master"
              }
            }]
          end

          before do
            allow_any_instance_of(Dependabot::GitCommitChecker)
              .to receive(:branch_or_ref_in_release?)
              .and_return(false)
            git_url = "https://github.com/hvssle/onfido.git"
            git_header = {
              "content-type" => "application/x-git-upload-pack-advertisement"
            }
            stub_request(:get, git_url + "/info/refs?service=git-upload-pack")
              .to_return(
                status: 200,
                body: fixture("git", "upload_packs", "onfido"),
                headers: git_header
              )
            allow(checker)
              .to receive(:latest_resolvable_version_details)
              .and_call_original
            allow(checker)
              .to receive(:latest_resolvable_version_details)
              .with(remove_git_source: true)
              .and_return(version: Dependabot::Bundler::Version.new("2.0.0"))
          end

          it { is_expected.to be_nil }
        end
      end

      context "when the gem under consideration is not the one we're checking" do
        let(:dependency_files) { bundler_project_dependency_files("git_source") }
        let(:dependency_name) { "statesman" }
        let(:current_version) { "1.2" }

        it { is_expected.to eq(Dependabot::Bundler::Version.new("3.4.1")) }

        context "when dealing with a private instance" do
          let(:dependency_files) { bundler_project_dependency_files("private_git_source") }
          let(:token) do
            Base64.encode64("x-access-token:#{github_token}").delete("\n")
          end

          before do
            stub_request(
              :get,
              "https://github.com/no-exist-sorry/prius.git/info/refs" \
              "?service=git-upload-pack"
            ).with(headers: { "Authorization" => "Basic #{token}" })
              .to_return(status: 401)
          end

          it "raises a helpful error" do
            expect { checker.latest_resolvable_version }
              .to raise_error do |error|
                expect(error).to be_a(Dependabot::GitDependenciesNotReachable)
                expect(error.dependency_urls)
                  .to eq(["git@github.com:no-exist-sorry/prius"])
              end
          end
        end

        context "when dealing with a bad reference" do
          let(:dependency_files) { bundler_project_dependency_files("bad_ref") }

          before do
            stub_request(:get, "https://github.com/dependabot-fixtures/prius")
              .to_return(status: 200)
          end

          it "raises a helpful error" do
            expect { checker.latest_resolvable_version }
              .to raise_error do |error|
                expect(error).to be_a Dependabot::GitDependencyReferenceNotFound
                expect(error.dependency).to eq("prius")
              end
          end
        end

        context "when dealing with a bad branch" do
          let(:dependency_files) { bundler_project_dependency_files("bad_branch") }

          it { is_expected.to eq(Dependabot::Bundler::Version.new("3.4.1")) }
        end
      end
    end

    context "when given a Gemfile that specifies a Ruby version" do
      let(:dependency_files) { bundler_project_dependency_files("explicit_ruby") }
      let(:dependency_name) { "statesman" }
      let(:requirements) do
        [{ file: "Gemfile", requirement: "~> 1.2.0", groups: [], source: nil }]
      end

      it { is_expected.to eq(Dependabot::Bundler::Version.new("3.4.1")) }

      context "when the instance is old" do
        let(:dependency_files) { bundler_project_dependency_files("explicit_ruby_old") }

        it "Gem version is 2.0.1" do
          skip "This test intermittently fails, which often trips up external contributors"
          expect(latest_resolvable_version).to eq(Dependabot::Bundler::Version.new("2.0.1"))
        end
      end
    end

    context "with a gemspec and a Gemfile" do
      let(:dependency_files) { bundler_project_dependency_files("imports_gemspec_small_example_no_lockfile") }
      let(:requirements) do
        [{
          file: "Gemfile",
          requirement: "~> 1.2.0",
          groups: [],
          source: nil
        }, {
          file: "example.gemspec",
          requirement: "~> 1.0",
          groups: [],
          source: nil
        }]
      end

      it "doesn't just fall back to latest_version" do
        expect(checker.latest_resolvable_version)
          .to eq(Dependabot::Bundler::Version.new("1.13.0"))
      end

      context "when the gemspec has a path" do
        let(:dependency_files) { bundler_project_dependency_files("imports_gemspec_from_path") }
        let(:requirements) do
          [{
            file: "Gemfile",
            requirement: "~> 1.2.0",
            groups: [],
            source: nil
          }, {
            file: "subdir/example.gemspec",
            requirement: "~> 1.0",
            groups: [],
            source: nil
          }]
        end

        it "doesn't just fall back to latest_version" do
          expect(checker.latest_resolvable_version)
            .to eq(Dependabot::Bundler::Version.new("1.13.0"))
        end
      end

      context "when an old required ruby is specified in the gemspec" do
        let(:dependency_files) { bundler_project_dependency_files("imports_gemspec_old_required_ruby_no_lockfile") }
        let(:dependency_name) { "statesman" }

        it "takes the minimum ruby version into account" do
          expect(checker.latest_resolvable_version)
            .to eq(Dependabot::Bundler::Version.new("2.0.1"))
        end
      end

      context "when the Gemfile doesn't import the gemspec" do
        let(:dependency_files) { bundler_project_dependency_files("gemspec_not_imported_no_lockfile") }

        it "falls back to latest_version" do
          expect(checker.latest_resolvable_version)
            .to eq(Dependabot::Bundler::Version.new("1.13.0"))
        end
      end
    end

    context "with only a gemspec" do
      let(:dependency_files) { bundler_project_dependency_files("gemspec_small_example_no_lockfile") }

      it "falls back to latest_version" do
        dummy_version_resolver =
          checker.send(:version_resolver, remove_git_source: false)
        dummy_version = Dependabot::Bundler::Version.new("0.5.0")
        allow(checker)
          .to receive(:version_resolver)
          .and_return(dummy_version_resolver)
        expect(dummy_version_resolver)
          .to receive(:latest_version_details)
          .and_return(version: dummy_version)
        expect(checker.latest_resolvable_version).to eq(dummy_version)
      end
    end

    context "with only a Gemfile" do
      let(:dependency_files) { bundler_project_dependency_files("no_lockfile") }

      it "doesn't just fall back to latest_version" do
        expect(checker.latest_resolvable_version)
          .to eq(Dependabot::Bundler::Version.new("1.13.0"))
      end

      context "when dealing with a gem with a private git source" do
        let(:dependency_files) { bundler_project_dependency_files("private_git_source_no_lockfile") }
        let(:token) do
          Base64.encode64("x-access-token:#{github_token}").delete("\n")
        end

        before do
          stub_request(
            :get,
            "https://github.com/dependabot-fixtures/does-not-exist.git/info/refs" \
            "?service=git-upload-pack"
          ).with(headers: { "Authorization" => "Basic #{token}" })
            .to_return(status: 401)
        end

        it "raises a helpful error" do
          expect { checker.latest_resolvable_version }
            .to raise_error do |error|
              expect(error).to be_a(Dependabot::GitDependenciesNotReachable)
              expect(error.dependency_urls)
                .to eq(["git@github.com:dependabot-fixtures/does-not-exist"])
            end
        end
      end

      context "when dealing with a gem with a private github source" do
        let(:dependency_files) { bundler_project_dependency_files("private_github_source_no_lockfile") }
        let(:token) do
          Base64.encode64("x-access-token:#{github_token}").delete("\n")
        end

        before do
          stub_request(
            :get,
            "https://github.com/dependabot-fixtures/does-not-exist.git/info/refs" \
            "?service=git-upload-pack"
          ).with(headers: { "Authorization" => "Basic #{token}" })
            .to_return(status: 401)
        end

        it "raises a helpful error" do
          expect { checker.latest_resolvable_version }
            .to raise_error do |error|
              expect(error).to be_a(Dependabot::GitDependenciesNotReachable)
              expect(error.dependency_urls)
                .to eq(["https://github.com/dependabot-fixtures/does-not-exist.git"])
            end
        end
      end

      context "when the git request raises a timeout" do
        let(:dependency_files) { bundler_project_dependency_files("private_git_source_no_lockfile") }
        let(:token) do
          Base64.encode64("x-access-token:#{github_token}").delete("\n")
        end

        before do
          stub_request(
            :get,
            "https://github.com/dependabot-fixtures/does-not-exist.git/info/refs" \
            "?service=git-upload-pack"
          ).with(headers: { "Authorization" => "Basic #{token}" })
            .to_raise(Excon::Error::Timeout)
        end

        it "raises a helpful error" do
          expect { checker.latest_resolvable_version }
            .to raise_error do |error|
              expect(error).to be_a(Dependabot::GitDependenciesNotReachable)
              expect(error.dependency_urls)
                .to eq(["git@github.com:dependabot-fixtures/does-not-exist"])
            end
        end
      end
    end

    context "with a gem that depends on bundler" do
      let(:dependency_files) { bundler_project_dependency_files("guard_bundler") }
      let(:requirements) do
        [{ file: "Gemfile", requirement: "~> 2.2.1, <= 3.0.0", groups: [], source: nil }]
      end
      let(:dependency_name) { "guard-bundler" }
      let(:current_version) { "2.2.1" }

      context "when using bundler v2" do
        it { is_expected.to eq(Dependabot::Bundler::Version.new("3.0.0")) }
      end
    end
  end

  describe "#preferred_resolvable_version" do
    subject { checker.preferred_resolvable_version }

    include_context "when stubbing rubygems compact index"
    include_context "when stubbing rubygems versions api"

    it { is_expected.to eq(Dependabot::Bundler::Version.new("1.13.0")) }

    context "with a security vulnerability" do
      let(:security_advisories) do
        [
          Dependabot::SecurityAdvisory.new(
            dependency_name: dependency_name,
            package_manager: "bundler",
            vulnerable_versions: ["<= 1.4.0"]
          )
        ]
      end

      it { is_expected.to eq(Dependabot::Bundler::Version.new("1.5.0")) }
    end
  end

  describe "#latest_resolvable_version_with_no_unlock" do
    subject { checker.latest_resolvable_version_with_no_unlock }

    include_context "when stubbing rubygems compact index"
    include_context "when stubbing rubygems versions api"

    context "when given a gem from rubygems" do
      it { is_expected.to eq(Dependabot::Bundler::Version.new("1.4.0")) }

      context "with a version conflict at the latest version" do
        let(:dependency_files) { bundler_project_dependency_files("version_conflict_no_req_change") }
        let(:dependency_name) { "ibandit" }
        let(:current_version) { "0.1.0" }
        let(:requirements) do
          [{ file: "Gemfile", requirement: "~> 0.1", groups: [], source: nil }]
        end

        # The latest version of ibandit is 0.8.5, but 0.3.4 is the latest
        # version compatible with the version of i18n in the Gemfile.
        it { is_expected.to eq(Dependabot::Bundler::Version.new("0.3.4")) }
      end
    end

    context "with a sub-dependency" do
      let(:dependency_files) { bundler_project_dependency_files("subdependency") }
      let(:requirements) { [] }
      let(:dependency_name) { "i18n" }
      let(:current_version) { "0.7.0.beta1" }

      it { is_expected.to eq(Dependabot::Bundler::Version.new("0.7.0")) }
    end
  end

  describe "#updated_requirements" do
    subject(:updated_requirements) { checker.updated_requirements }

    include_context "when stubbing rubygems compact index"
    include_context "when stubbing rubygems versions api"

    let(:requirements_updater) do
      Dependabot::Bundler::UpdateChecker::RequirementsUpdater
    end

    before do
      allow(requirements_updater).to receive(:new).and_call_original
    end

    context "with a Gemfile and a Gemfile.lock" do
      let(:dependency_files) { bundler_project_dependency_files("gemfile") }
      let(:dependency_name) { "business" }
      let(:current_version) { "1.4.0" }

      let(:requirements) do
        [{
          file: "Gemfile",
          requirement: "~> 1.4.0",
          groups: [:default],
          source: nil
        }]
      end

      it "delegates to Bundler::RequirementsUpdater with the right params" do
        expect(requirements_updater)
          .to receive(:new).with(
            requirements: requirements,
            update_strategy: Dependabot::RequirementsUpdateStrategy::BumpVersions,
            latest_version: "1.13.0",
            latest_resolvable_version: "1.13.0",
            updated_source: nil
          ).and_call_original

        expect(updated_requirements.count).to eq(1)
        expect(updated_requirements.first[:requirement]).to eq("~> 1.13.0")
      end

      context "with a security vulnerability" do
        let(:security_advisories) do
          [
            Dependabot::SecurityAdvisory.new(
              dependency_name: dependency_name,
              package_manager: "bundler",
              vulnerable_versions: ["<= 1.4.0"]
            )
          ]
        end

        it "delegates to Bundler::RequirementsUpdater with the right params" do
          expect(requirements_updater)
            .to receive(:new).with(
              requirements: requirements,
              update_strategy: Dependabot::RequirementsUpdateStrategy::BumpVersions,
              latest_version: "1.13.0",
              latest_resolvable_version: "1.5.0",
              updated_source: nil
            ).and_call_original

          expect(updated_requirements.count).to eq(1)
          expect(updated_requirements.first[:requirement]).to eq("~> 1.5.0")
        end
      end

      context "with a sub-dependency" do
        let(:dependency_files) { bundler_project_dependency_files("subdependency") }
        let(:requirements) { [] }
        let(:dependency_name) { "i18n" }
        let(:current_version) { "0.7.0.beta1" }

        it { is_expected.to eq([]) }
      end

      context "with a gems.rb and gems.locked" do
        let(:dependency_files) { bundler_project_dependency_files("gems_rb") }

        let(:requirements) do
          [{
            file: "gems.rb",
            requirement: "~> 1.4.0",
            groups: [:default],
            source: nil
          }]
        end

        it "delegates to Bundler::RequirementsUpdater" do
          expect(requirements_updater)
            .to receive(:new).with(
              requirements: requirements,
              update_strategy: Dependabot::RequirementsUpdateStrategy::BumpVersions,
              latest_version: "1.13.0",
              latest_resolvable_version: "1.13.0",
              updated_source: nil
            ).and_call_original

          expect(updated_requirements.count).to eq(1)
          expect(updated_requirements.first[:requirement]).to eq("~> 1.13.0")
          expect(updated_requirements.first[:file]).to eq("gems.rb")
        end
      end

      context "when dealing with a gem with a git source" do
        let(:dependency_files) { bundler_project_dependency_files("git_source_with_version_gemfile") }
        let(:dependency_name) { "dependabot-test-ruby-package" }
        let(:current_version) { "81073f9462f228c6894e3e384d0718def310d99f" }
        let(:requirements) do
          [{
            file: "Gemfile",
            requirement: "~> 1.0.0",
            groups: [:default],
            source: {
              type: "git",
              url: "https://github.com/dependabot-fixtures/" \
                   "dependabot-test-ruby-package",
              branch: "master",
              ref: "master"
            }
          }]
        end

        before do
          allow_any_instance_of(Dependabot::GitCommitChecker)
            .to receive(:branch_or_ref_in_release?)
            .and_return(false)
          stub_request(
            :get, rubygems_url + "versions/dependabot-test-ruby-package.json"
          ).to_return(status: 404)
          git_url = "https://github.com/dependabot-fixtures/" \
                    "dependabot-test-ruby-package.git"
          git_header = {
            "content-type" => "application/x-git-upload-pack-advertisement"
          }
          stub_request(:get, git_url + "/info/refs?service=git-upload-pack")
            .to_return(
              status: 200,
              body: fixture(
                "git",
                "upload_packs",
                "dependabot-test-ruby-package"
              ),
              headers: git_header
            )
        end

        it "delegates to Bundler::RequirementsUpdater with the right params" do
          skip "Skipping as this fails after removing a feature flag that is rolled out to 100%"
          expect(requirements_updater)
            .to receive(:new).with(
              requirements: requirements,
              update_strategy: Dependabot::RequirementsUpdateStrategy::BumpVersions,
              latest_version: "1.0.1",
              latest_resolvable_version: "1.0.1",
              updated_source: requirements.first[:source]
            ).and_call_original

          expect(updated_requirements.count).to eq(1)
          expect(updated_requirements.first[:requirement])
            .to start_with("~> 1.")
        end

        context "when the reference is pinned" do
          let(:dependency_files) { bundler_project_dependency_files("git_source") }

          let(:dependency_name) { "business" }
          let(:current_version) { "a1b78a929dac93a52f08db4f2847d76d6cfe39bd" }
          let(:requirements) do
            [{
              file: "Gemfile",
              requirement: ">= 0",
              groups: [],
              source: {
                type: "git",
                url: "https://github.com/dependabot-fixtures/business",
                branch: "master",
                ref: "a1b78a9"
              }
            }]
          end

          context "when the reference is not included in the new version" do
            before do
              stub_request(:get, rubygems_url + "versions/business.json")
                .to_return(status: 404, body: "This rubygem could not be found.")
            end

            it "delegates to Bundler::RequirementsUpdater" do
              skip "Skipping as this fails after removing a feature flag that is rolled out to 100%"
              expect(requirements_updater)
                .to receive(:new).with(
                  requirements: requirements,
                  update_strategy: Dependabot::RequirementsUpdateStrategy::BumpVersions,
                  latest_version: /^2./,
                  latest_resolvable_version: /^1./,
                  updated_source: requirements.first[:source]
                ).and_call_original

              expect(updated_requirements.count).to eq(1)
              expect(updated_requirements.first[:requirement]).to eq(">= 0")
              expect(updated_requirements.first[:source]).not_to be_nil
            end
          end
        end
      end
    end

    context "with a Gemfile, a Gemfile.lock and a gemspec" do
      let(:dependency_files) { bundler_project_dependency_files("imports_gemspec") }
      let(:dependency_name) { "business" }
      let(:current_version) { "1.4.0" }

      let(:requirements) do
        [{
          file: "Gemfile",
          requirement: "~> 1.4.0",
          groups: [:default],
          source: nil
        }, {
          file: "example.gemspec",
          requirement: "~> 1.0",
          groups: [:default],
          source: nil
        }]
      end

      it "delegates to Bundler::RequirementsUpdater with the right params" do
        expect(requirements_updater)
          .to receive(:new).with(
            requirements: requirements,
            update_strategy: Dependabot::RequirementsUpdateStrategy::BumpVersions,
            latest_version: "1.13.0",
            latest_resolvable_version: "1.13.0",
            updated_source: requirements.first[:source]
          ).and_call_original

        expect(updated_requirements.count).to eq(2)
        expect(updated_requirements.first[:requirement]).to eq("~> 1.13.0")
        expect(updated_requirements.last[:requirement]).to eq("~> 1.0")
      end
    end

    context "with a Gemfile and a gemspec" do
      let(:dependency_files) { bundler_project_dependency_files("imports_gemspec_small_example_no_lockfile") }
      let(:dependency_name) { "business" }
      let(:current_version) { nil }

      let(:requirements) do
        [{
          file: "Gemfile",
          requirement: "~> 1.4.0",
          groups: [:default],
          source: nil
        }, {
          file: "example.gemspec",
          requirement: "~> 1.0",
          groups: [:default],
          source: nil
        }]
      end

      it "delegates to Bundler::RequirementsUpdater with the right params" do
        expect(requirements_updater)
          .to receive(:new).with(
            requirements: requirements,
            update_strategy: Dependabot::RequirementsUpdateStrategy::BumpVersionsIfNecessary,
            latest_version: "1.13.0",
            latest_resolvable_version: "1.13.0",
            updated_source: requirements.first[:source]
          ).and_call_original

        expect(updated_requirements.count).to eq(2)
        expect(updated_requirements.first[:requirement]).to eq("~> 1.13.0")
        expect(updated_requirements.last[:requirement]).to eq("~> 1.0")
      end
    end

    context "with a Gemfile only" do
      let(:dependency_files) { bundler_project_dependency_files("no_lockfile") }
      let(:dependency_name) { "business" }
      let(:current_version) { nil }
      let(:requirements) do
        [{
          file: "Gemfile",
          requirement: "~> 1.4.0",
          groups: [:default],
          source: nil
        }]
      end

      it "delegates to Bundler::RequirementsUpdater with the right params" do
        expect(requirements_updater)
          .to receive(:new).with(
            requirements: requirements,
            update_strategy: Dependabot::RequirementsUpdateStrategy::BumpVersionsIfNecessary,
            latest_version: "1.13.0",
            latest_resolvable_version: "1.13.0",
            updated_source: requirements.first[:source]
          ).and_call_original

        expect(updated_requirements.count).to eq(1)
        expect(updated_requirements.first[:requirement]).to eq("~> 1.13.0")
      end
    end

    context "with a gemspec only" do
      let(:dependency_files) { bundler_project_dependency_files("gemspec_no_lockfile") }
      let(:dependency_name) { "business" }
      let(:current_version) { nil }
      let(:requirements) do
        [{
          file: "example.gemspec",
          requirement: "~> 0.9",
          groups: ["runtime"],
          source: nil
        }]
      end

      it "delegates to Bundler::RequirementsUpdater with the right params" do
        expect(requirements_updater)
          .to receive(:new).with(
            requirements: requirements,
            update_strategy: Dependabot::RequirementsUpdateStrategy::BumpVersionsIfNecessary,
            latest_version: "1.13.0",
            latest_resolvable_version: "1.13.0",
            updated_source: requirements.first[:source]
          ).and_call_original

        expect(updated_requirements.count).to eq(1)
        expect(updated_requirements.first[:requirement]).to eq(">= 0.9, < 2.0")
      end
    end
  end

  describe "#requirements_unlocked_or_can_be?" do
    subject { checker.requirements_unlocked_or_can_be? }

    context "with a Gemfile dependency that is already unlocked" do
      let(:dependency_files) { bundler_project_dependency_files("version_not_specified") }
      let(:requirements) do
        [{ file: "Gemfile", requirement: ">= 0", groups: [], source: nil }]
      end

      it { is_expected.to be(true) }

      context "when the lockfile-only requirements update strategy is set" do
        let(:requirements_update_strategy) { Dependabot::RequirementsUpdateStrategy::LockfileOnly }

        it { is_expected.to be(true) }
      end
    end

    context "with a sub-dependency" do
      let(:dependency_files) { bundler_project_dependency_files("subdependency") }
      let(:requirements) { [] }
      let(:dependency_name) { "i18n" }
      let(:current_version) { "0.7.0.beta1" }

      it { is_expected.to be(true) }
    end

    context "with a Gemfile dependency that can be unlocked" do
      let(:dependency_files) { bundler_project_dependency_files("gemfile") }
      let(:requirements) do
        [{ file: "Gemfile", requirement: req, groups: [], source: nil }]
      end
      let(:req) { "~> 1.4.0" }

      it { is_expected.to be(true) }

      context "with multiple requirements" do
        let(:dependency_files) { bundler_project_dependency_files("version_between_bounds_gemfile") }
        let(:req) { "> 1.0.0, < 1.5.0" }

        it { is_expected.to be(true) }
      end

      context "when the lockfile-only requirements update strategy is set" do
        let(:requirements_update_strategy) { Dependabot::RequirementsUpdateStrategy::LockfileOnly }

        it { is_expected.to be(false) }
      end
    end

    # For now we always let git dependencies through
    context "with a Gemfile dependency that is a git dependency" do
      let(:dependency_files) { bundler_project_dependency_files("git_source_no_ref") }
      let(:requirements) do
        [{
          file: "Gemfile",
          requirement: ">= 0",
          groups: [],
          source: {
            type: "git",
            url: "https://github.com/dependabot-fixtures/business",
            branch: "master",
            ref: "master"
          }
        }]
      end

      it { is_expected.to be(true) }
    end

    context "with a Gemfile with a function version" do
      let(:dependency_files) { bundler_project_dependency_files("function_version_gemfile") }
      let(:requirements) do
        [{ file: "Gemfile", requirement: "1.0.0", groups: [], source: nil }]
      end

      it { is_expected.to be(false) }
    end
  end
end
