diff options
Diffstat (limited to 'spec/bundler/bundler/bundler_spec.rb')
-rw-r--r-- | spec/bundler/bundler/bundler_spec.rb | 490 |
1 files changed, 490 insertions, 0 deletions
diff --git a/spec/bundler/bundler/bundler_spec.rb b/spec/bundler/bundler/bundler_spec.rb new file mode 100644 index 0000000000..194d6752b2 --- /dev/null +++ b/spec/bundler/bundler/bundler_spec.rb @@ -0,0 +1,490 @@ +# encoding: utf-8 +# frozen_string_literal: true + +require "bundler" +require "tmpdir" + +RSpec.describe Bundler do + describe "#load_gemspec_uncached" do + let(:app_gemspec_path) { tmp("test.gemspec") } + subject { Bundler.load_gemspec_uncached(app_gemspec_path) } + + context "with incorrect YAML file" do + before do + File.open(app_gemspec_path, "wb") do |f| + f.write strip_whitespace(<<-GEMSPEC) + --- + {:!00 ao=gu\g1= 7~f + GEMSPEC + end + end + + it "catches YAML syntax errors" do + expect { subject }.to raise_error(Bundler::GemspecError, /error while loading `test.gemspec`/) + end + + context "on Rubies with a settable YAML engine", :if => defined?(YAML::ENGINE) do + context "with Syck as YAML::Engine" do + it "raises a GemspecError after YAML load throws ArgumentError" do + orig_yamler = YAML::ENGINE.yamler + YAML::ENGINE.yamler = "syck" + + expect { subject }.to raise_error(Bundler::GemspecError) + + YAML::ENGINE.yamler = orig_yamler + end + end + + context "with Psych as YAML::Engine" do + it "raises a GemspecError after YAML load throws Psych::SyntaxError" do + orig_yamler = YAML::ENGINE.yamler + YAML::ENGINE.yamler = "psych" + + expect { subject }.to raise_error(Bundler::GemspecError) + + YAML::ENGINE.yamler = orig_yamler + end + end + end + end + + context "with correct YAML file", :if => defined?(Encoding) do + it "can load a gemspec with unicode characters with default ruby encoding" do + # spec_helper forces the external encoding to UTF-8 but that's not the + # default until Ruby 2.0 + verbose = $VERBOSE + $VERBOSE = false + encoding = Encoding.default_external + Encoding.default_external = "ASCII" + $VERBOSE = verbose + + File.open(app_gemspec_path, "wb") do |file| + file.puts <<-GEMSPEC.gsub(/^\s+/, "") + # -*- encoding: utf-8 -*- + Gem::Specification.new do |gem| + gem.author = "André the Giant" + end + GEMSPEC + end + + expect(subject.author).to eq("André the Giant") + + verbose = $VERBOSE + $VERBOSE = false + Encoding.default_external = encoding + $VERBOSE = verbose + end + end + + it "sets loaded_from" do + app_gemspec_path.open("w") do |f| + f.puts <<-GEMSPEC + Gem::Specification.new do |gem| + gem.name = "validated" + end + GEMSPEC + end + + expect(subject.loaded_from).to eq(app_gemspec_path.expand_path.to_s) + end + + context "validate is true" do + subject { Bundler.load_gemspec_uncached(app_gemspec_path, true) } + + it "validates the specification" do + app_gemspec_path.open("w") do |f| + f.puts <<-GEMSPEC + Gem::Specification.new do |gem| + gem.name = "validated" + end + GEMSPEC + end + expect(Bundler.rubygems).to receive(:validate).with have_attributes(:name => "validated") + subject + end + end + + context "with gemspec containing local variables" do + before do + File.open(app_gemspec_path, "wb") do |f| + f.write strip_whitespace(<<-GEMSPEC) + must_not_leak = true + Gem::Specification.new do |gem| + gem.name = "leak check" + end + GEMSPEC + end + end + + it "should not pollute the TOPLEVEL_BINDING" do + subject + expect(TOPLEVEL_BINDING.eval("local_variables")).to_not include(:must_not_leak) + end + end + end + + describe "#which" do + let(:executable) { "executable" } + let(:path) { %w[/a /b c ../d /e] } + let(:expected) { "executable" } + + before do + ENV["PATH"] = path.join(File::PATH_SEPARATOR) + + allow(File).to receive(:file?).and_return(false) + allow(File).to receive(:executable?).and_return(false) + if expected + expect(File).to receive(:file?).with(expected).and_return(true) + expect(File).to receive(:executable?).with(expected).and_return(true) + end + end + + subject { described_class.which(executable) } + + shared_examples_for "it returns the correct executable" do + it "returns the expected file" do + expect(subject).to eq(expected) + end + end + + it_behaves_like "it returns the correct executable" + + context "when the executable in inside a quoted path" do + let(:expected) { "/e/executable" } + it_behaves_like "it returns the correct executable" + end + + context "when the executable is not found" do + let(:expected) { nil } + it_behaves_like "it returns the correct executable" + end + end + + describe "configuration" do + context "disable_shared_gems" do + it "should unset GEM_PATH with empty string" do + env = {} + expect(Bundler).to receive(:use_system_gems?).and_return(false) + Bundler.send(:configure_gem_path, env) + expect(env.keys).to include("GEM_PATH") + expect(env["GEM_PATH"]).to eq "" + end + end + end + + describe "#rm_rf" do + context "the directory is world writable" do + let(:bundler_ui) { Bundler.ui } + it "should raise a friendly error" do + allow(File).to receive(:exist?).and_return(true) + allow(bundler_fileutils).to receive(:remove_entry_secure).and_raise(ArgumentError) + allow(File).to receive(:world_writable?).and_return(true) + message = <<EOF +It is a security vulnerability to allow your home directory to be world-writable, and bundler can not continue. +You should probably consider fixing this issue by running `chmod o-w ~` on *nix. +Please refer to http://ruby-doc.org/stdlib-2.1.2/libdoc/fileutils/rdoc/FileUtils.html#method-c-remove_entry_secure for details. +EOF + expect(bundler_ui).to receive(:warn).with(message) + expect { Bundler.send(:rm_rf, bundled_app) }.to raise_error(Bundler::PathError) + end + end + end + + describe "#mkdir_p" do + it "creates a folder at the given path" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + Bundler.mkdir_p(bundled_app.join("foo", "bar")) + expect(bundled_app.join("foo", "bar")).to exist + end + + context "when mkdir_p requires sudo" do + it "creates a new folder using sudo" do + expect(Bundler).to receive(:requires_sudo?).and_return(true) + expect(Bundler).to receive(:sudo).and_return true + Bundler.mkdir_p(bundled_app.join("foo")) + end + end + + context "with :no_sudo option" do + it "forces mkdir_p to not use sudo" do + expect(Bundler).to receive(:requires_sudo?).and_return(true) + expect(Bundler).to_not receive(:sudo) + Bundler.mkdir_p(bundled_app.join("foo"), :no_sudo => true) + end + end + end + + describe "#user_home" do + context "home directory is set" do + it "should return the user home" do + path = "/home/oggy" + allow(Bundler.rubygems).to receive(:user_home).and_return(path) + allow(File).to receive(:directory?).with(path).and_return true + allow(File).to receive(:writable?).with(path).and_return true + expect(Bundler.user_home).to eq(Pathname(path)) + end + + context "is not a directory" do + it "should issue a warning and return a temporary user home" do + path = "/home/oggy" + allow(Bundler.rubygems).to receive(:user_home).and_return(path) + allow(File).to receive(:directory?).with(path).and_return false + allow(Etc).to receive(:getlogin).and_return("USER") + allow(Dir).to receive(:tmpdir).and_return("/TMP") + allow(FileTest).to receive(:exist?).with("/TMP/bundler/home").and_return(true) + expect(FileUtils).to receive(:mkpath).with("/TMP/bundler/home/USER") + message = <<EOF +`/home/oggy` is not a directory. +Bundler will use `/TMP/bundler/home/USER' as your home directory temporarily. +EOF + expect(Bundler.ui).to receive(:warn).with(message) + expect(Bundler.user_home).to eq(Pathname("/TMP/bundler/home/USER")) + end + end + + context "is not writable" do + let(:path) { "/home/oggy" } + let(:dotbundle) { "/home/oggy/.bundle" } + + it "should issue a warning and return a temporary user home" do + allow(Bundler.rubygems).to receive(:user_home).and_return(path) + allow(File).to receive(:directory?).with(path).and_return true + allow(File).to receive(:writable?).with(path).and_return false + allow(File).to receive(:directory?).with(dotbundle).and_return false + allow(Etc).to receive(:getlogin).and_return("USER") + allow(Dir).to receive(:tmpdir).and_return("/TMP") + allow(FileTest).to receive(:exist?).with("/TMP/bundler/home").and_return(true) + expect(FileUtils).to receive(:mkpath).with("/TMP/bundler/home/USER") + message = <<EOF +`/home/oggy` is not writable. +Bundler will use `/TMP/bundler/home/USER' as your home directory temporarily. +EOF + expect(Bundler.ui).to receive(:warn).with(message) + expect(Bundler.user_home).to eq(Pathname("/TMP/bundler/home/USER")) + end + + context ".bundle exists and have correct permissions" do + it "should return the user home" do + allow(Bundler.rubygems).to receive(:user_home).and_return(path) + allow(File).to receive(:directory?).with(path).and_return true + allow(File).to receive(:writable?).with(path).and_return false + allow(File).to receive(:directory?).with(dotbundle).and_return true + allow(File).to receive(:writable?).with(dotbundle).and_return true + expect(Bundler.user_home).to eq(Pathname(path)) + end + end + end + end + + context "home directory is not set" do + it "should issue warning and return a temporary user home" do + allow(Bundler.rubygems).to receive(:user_home).and_return(nil) + allow(Etc).to receive(:getlogin).and_return("USER") + allow(Dir).to receive(:tmpdir).and_return("/TMP") + allow(FileTest).to receive(:exist?).with("/TMP/bundler/home").and_return(true) + expect(FileUtils).to receive(:mkpath).with("/TMP/bundler/home/USER") + message = <<EOF +Your home directory is not set. +Bundler will use `/TMP/bundler/home/USER' as your home directory temporarily. +EOF + expect(Bundler.ui).to receive(:warn).with(message) + expect(Bundler.user_home).to eq(Pathname("/TMP/bundler/home/USER")) + end + end + end + + describe "#tmp_home_path" do + it "should create temporary user home" do + allow(Dir).to receive(:tmpdir).and_return("/TMP") + allow(FileTest).to receive(:exist?).with("/TMP/bundler/home").and_return(false) + expect(FileUtils).to receive(:mkpath).once.ordered.with("/TMP/bundler/home") + expect(FileUtils).to receive(:mkpath).once.ordered.with("/TMP/bundler/home/USER") + expect(File).to receive(:chmod).with(0o777, "/TMP/bundler/home") + expect(Bundler.tmp_home_path("USER", "")).to eq(Pathname("/TMP/bundler/home/USER")) + end + end + + describe "#requires_sudo?" do + let!(:tmpdir) { Dir.mktmpdir } + let(:bundle_path) { Pathname("#{tmpdir}/bundle") } + + def clear_cached_requires_sudo + # Private in ruby 1.8.7 + return unless Bundler.instance_variable_defined?(:@requires_sudo_ran) + Bundler.send(:remove_instance_variable, :@requires_sudo_ran) + Bundler.send(:remove_instance_variable, :@requires_sudo) + end + + before do + clear_cached_requires_sudo + allow(Bundler).to receive(:which).with("sudo").and_return("/usr/bin/sudo") + allow(Bundler).to receive(:bundle_path).and_return(bundle_path) + end + + after do + FileUtils.rm_rf(tmpdir) + clear_cached_requires_sudo + end + + subject { Bundler.requires_sudo? } + + context "bundle_path doesn't exist" do + it { should be false } + + context "and parent dir can't be written" do + before do + FileUtils.chmod(0o500, tmpdir) + end + + it { should be true } + end + + context "with unwritable files in a parent dir" do + # Regression test for https://github.com/bundler/bundler/pull/6316 + # It doesn't matter if there are other unwritable files so long as + # bundle_path can be created + before do + file = File.join(tmpdir, "unrelated_file") + FileUtils.touch(file) + FileUtils.chmod(0o400, file) + end + + it { should be false } + end + end + + context "bundle_path exists" do + before do + FileUtils.mkdir_p(bundle_path) + end + + it { should be false } + + context "and is unwritable" do + before do + FileUtils.chmod(0o500, bundle_path) + end + + it { should be true } + end + end + end + + describe "#requires_sudo?" do + before do + allow(Bundler).to receive(:which).with("sudo").and_return("/usr/bin/sudo") + FileUtils.mkdir_p("tmp/vendor/bundle") + FileUtils.mkdir_p("tmp/vendor/bin_dir") + end + after do + FileUtils.rm_rf("tmp/vendor/bundle") + FileUtils.rm_rf("tmp/vendor/bin_dir") + if Bundler.respond_to?(:remove_instance_variable) + Bundler.remove_instance_variable(:@requires_sudo_ran) + Bundler.remove_instance_variable(:@requires_sudo) + else + # TODO: Remove these code when Bundler drops Ruby 1.8.7 support + Bundler.send(:remove_instance_variable, :@requires_sudo_ran) + Bundler.send(:remove_instance_variable, :@requires_sudo) + end + end + context "writable paths" do + it "should return false and display nothing" do + allow(Bundler).to receive(:bundle_path).and_return(Pathname("tmp/vendor/bundle")) + expect(Bundler.ui).to_not receive(:warn) + expect(Bundler.requires_sudo?).to eq(false) + end + end + context "unwritable paths" do + before do + FileUtils.touch("tmp/vendor/bundle/unwritable1.txt") + FileUtils.touch("tmp/vendor/bundle/unwritable2.txt") + FileUtils.touch("tmp/vendor/bin_dir/unwritable3.txt") + FileUtils.chmod(0o400, "tmp/vendor/bundle/unwritable1.txt") + FileUtils.chmod(0o400, "tmp/vendor/bundle/unwritable2.txt") + FileUtils.chmod(0o400, "tmp/vendor/bin_dir/unwritable3.txt") + end + it "should return true and display warn message" do + allow(Bundler).to receive(:bundle_path).and_return(Pathname("tmp/vendor/bundle")) + bin_dir = Pathname("tmp/vendor/bin_dir/") + + # allow File#writable? to be called with args other than the stubbed on below + allow(File).to receive(:writable?).and_call_original + + # fake make the directory unwritable + allow(File).to receive(:writable?).with(bin_dir).and_return(false) + allow(Bundler).to receive(:system_bindir).and_return(Pathname("tmp/vendor/bin_dir/")) + message = <<-MESSAGE.chomp +Following files may not be writable, so sudo is needed: + tmp/vendor/bin_dir/ + tmp/vendor/bundle/unwritable1.txt + tmp/vendor/bundle/unwritable2.txt +MESSAGE + expect(Bundler.ui).to receive(:warn).with(message) + expect(Bundler.requires_sudo?).to eq(true) + end + end + end + + context "user cache dir" do + let(:home_path) { Pathname.new(ENV["HOME"]) } + + let(:xdg_data_home) { home_path.join(".local") } + let(:xdg_cache_home) { home_path.join(".cache") } + let(:xdg_config_home) { home_path.join(".config") } + + let(:bundle_user_home_default) { home_path.join(".bundle") } + let(:bundle_user_home_custom) { xdg_data_home.join("bundle") } + + let(:bundle_user_cache_default) { bundle_user_home_default.join("cache") } + let(:bundle_user_cache_custom) { xdg_cache_home.join("bundle") } + + let(:bundle_user_config_default) { bundle_user_home_default.join("config") } + let(:bundle_user_config_custom) { xdg_config_home.join("bundle") } + + let(:bundle_user_plugin_default) { bundle_user_home_default.join("plugin") } + let(:bundle_user_plugin_custom) { xdg_data_home.join("bundle").join("plugin") } + + describe "#user_bundle_path" do + before do + allow(Bundler.rubygems).to receive(:user_home).and_return(home_path) + end + + it "should use the default home path" do + expect(Bundler.user_bundle_path).to eq(bundle_user_home_default) + expect(Bundler.user_bundle_path("home")).to eq(bundle_user_home_default) + expect(Bundler.user_bundle_path("cache")).to eq(bundle_user_cache_default) + expect(Bundler.user_cache).to eq(bundle_user_cache_default) + expect(Bundler.user_bundle_path("config")).to eq(bundle_user_config_default) + expect(Bundler.user_bundle_path("plugin")).to eq(bundle_user_plugin_default) + end + + it "should use custom home path as root for other paths" do + ENV["BUNDLE_USER_HOME"] = bundle_user_home_custom.to_s + expect(Bundler.user_bundle_path).to eq(bundle_user_home_custom) + expect(Bundler.user_bundle_path("home")).to eq(bundle_user_home_custom) + expect(Bundler.user_bundle_path("cache")).to eq(bundle_user_home_custom.join("cache")) + expect(Bundler.user_cache).to eq(bundle_user_home_custom.join("cache")) + expect(Bundler.user_bundle_path("config")).to eq(bundle_user_home_custom.join("config")) + expect(Bundler.user_bundle_path("plugin")).to eq(bundle_user_home_custom.join("plugin")) + end + + it "should use all custom paths, except home" do + ENV.delete("BUNDLE_USER_HOME") + ENV["BUNDLE_USER_CACHE"] = bundle_user_cache_custom.to_s + ENV["BUNDLE_USER_CONFIG"] = bundle_user_config_custom.to_s + ENV["BUNDLE_USER_PLUGIN"] = bundle_user_plugin_custom.to_s + expect(Bundler.user_bundle_path).to eq(bundle_user_home_default) + expect(Bundler.user_bundle_path("home")).to eq(bundle_user_home_default) + expect(Bundler.user_bundle_path("cache")).to eq(bundle_user_cache_custom) + expect(Bundler.user_cache).to eq(bundle_user_cache_custom) + expect(Bundler.user_bundle_path("config")).to eq(bundle_user_config_custom) + expect(Bundler.user_bundle_path("plugin")).to eq(bundle_user_plugin_custom) + end + end + end +end |