# Copyright (c) HashiCorp, Inc. # SPDX-License-Identifier: BUSL-1.1 require "tmpdir" require_relative "../base" require "vagrant/bundler" describe Vagrant::Bundler::SolutionFile do let(:plugin_path) { Pathname.new(tmpdir) + "plugin_file" } let(:solution_path) { Pathname.new(tmpdir) + "solution_file" } let(:tmpdir) { @tmpdir ||= Dir.mktmpdir("vagrant-bundler-test") } let(:subject) { described_class.new( plugin_file: plugin_path, solution_file: solution_path ) } after do if @tmpdir FileUtils.rm_rf(@tmpdir) @tmpdir = nil end end describe "#initialize" do context "file paths" do context "with solution_file not provided" do let(:subject) { described_class.new(plugin_file: plugin_path) } it "should set the plugin_file" do expect(subject.plugin_file.to_s).to eq(plugin_path.to_s) end it "should set solution path to same directory" do expect(subject.solution_file.to_s).to eq(plugin_path.to_s + ".sol") end end context "with custom solution_file provided" do let(:subject) { described_class. new(plugin_file: plugin_path, solution_file: solution_path) } it "should set the plugin file path" do expect(subject.plugin_file.to_s).to eq(plugin_path.to_s) end it "should set the solution file path to given value" do expect(subject.solution_file.to_s).to eq(solution_path.to_s) end end end context "initialization behavior" do context "on creation" do before { expect_any_instance_of(described_class).to receive(:load) } it "should load solution file during initialization" do subject end end it "should be invalid by default" do expect(subject.valid?).to be_falsey end end end describe "#dependency_list=" do it "should accept a list of Gem::Dependency instances" do list = ["dep1", "dep2"].map{ |x| Gem::Dependency.new(x) } subject.dependency_list = list expect(subject.dependency_list.map(&:dependency)).to eq(list) end it "should error if list includes instance not Gem::Dependency" do list = ["dep1", "dep2"].map{ |x| Gem::Dependency.new(x) } << :invalid expect{ subject.dependency_list = list }.to raise_error(TypeError) end it "should convert list into resolver dependency request" do list = ["dep1", "dep2"].map{ |x| Gem::Dependency.new(x) } subject.dependency_list = list subject.dependency_list.each do |dep| expect(dep).to be_a(Gem::Resolver::DependencyRequest) end end it "should freeze the new dependency list" do list = ["dep1", "dep2"].map{ |x| Gem::Dependency.new(x) } subject.dependency_list = list expect(subject.dependency_list).to be_frozen end end describe "#delete!" do context "when file does not exist" do before { subject.solution_file.delete if subject.solution_file.exist? } it "should return false" do expect(subject.delete!).to be_falsey end it "should not exist" do subject.delete! expect(subject.solution_file.exist?).to be_falsey end end context "when file does exist" do before { subject.solution_file.write('x') } it "should return true" do expect(subject.delete!).to be_truthy end it "should not exist" do expect(subject.solution_file.exist?).to be_truthy subject.delete! expect(subject.solution_file.exist?).to be_falsey end end end describe "store!" do context "when plugin file does not exist" do before { subject.plugin_file.delete if subject.plugin_file.exist? } it "should return false" do expect(subject.store!).to be_falsey end it "should not create a solution file" do subject.store! expect(subject.solution_file.exist?).to be_falsey end end context "when plugin file does exist" do before { subject.plugin_file.write("x") } it "should return true" do expect(subject.store!).to be_truthy end it "should create a solution file" do expect(subject.solution_file.exist?).to be_falsey subject.store! expect(subject.solution_file.exist?).to be_truthy end context "stored file" do let(:content) { @content ||= JSON.load(subject.solution_file.read) } before { subject.store! } after { @content = nil } it "should store JSON hash" do expect(content).to be_a(Hash) end it "should include dependencies key as array value" do expect(content["dependencies"]).to be_a(Array) end it "should include checksum key as string value" do expect(content["checksum"]).to be_a(String) end it "should include vagrant_version key as string value" do expect(content["vagrant_version"]).to be_a(String) end it "should include vagrant_version key that matches current version" do expect(content["vagrant_version"]).to eq(Vagrant::VERSION) end end end end describe "behavior" do context "when storing new solution set" do let(:deps) { ["dep1", "dep2"].map{ |n| Gem::Dependency.new(n) } } context "when plugin file does not exist" do before { subject.solution_file.delete if subject.solution_file.exist? } it "should not create a solution file" do subject.dependency_list = deps subject.store! expect(subject.solution_file.exist?).to be_falsey end end context "when plugin file does exist" do before { subject.plugin_file.write("x") } it "should create a solution file" do subject.dependency_list = deps subject.store! expect(subject.solution_file.exist?).to be_truthy end it "should update solution file instance to valid" do expect(subject.valid?).to be_falsey subject.dependency_list = deps subject.store! expect(subject.valid?).to be_truthy end context "when solution file does exist" do before do subject.dependency_list = deps subject.store! end it "should be a valid solution" do subject = described_class.new( plugin_file: plugin_path, solution_file: solution_path ) expect(subject.valid?).to be_truthy end it "should have expected dependency list" do subject = described_class.new( plugin_file: plugin_path, solution_file: solution_path ) expect(subject.dependency_list).to eq(deps) end context "when plugin file has been changed" do before { subject.plugin_file.write("xy") } it "should not be a valid solution" do subject = described_class.new( plugin_file: plugin_path, solution_file: solution_path ) expect(subject.valid?).to be_falsey end it "should have empty dependency list" do subject = described_class.new( plugin_file: plugin_path, solution_file: solution_path ) expect(subject.dependency_list).to be_empty end end end end end end describe "#load" do let(:plugin_file_exists) { false } let(:solution_file_exists) { false } let(:plugin_file_path) { "PLUGIN_FILE_PATH" } let(:solution_file_path) { "SOLUTION_FILE_PATH" } let(:plugin_file) { double("plugin-file") } let(:solution_file) { double("solution-file") } subject do described_class.new(plugin_file: plugin_file_path, solution_file: solution_file_path) end before do allow(Pathname).to receive(:new).with(plugin_file_path).and_return(plugin_file) allow(Pathname).to receive(:new).with(solution_file_path).and_return(solution_file) allow(plugin_file).to receive(:exist?).and_return(plugin_file_exists) allow(solution_file).to receive(:exist?).and_return(solution_file_exists) end context "when plugin file and solution file do not exist" do it "should not attempt to read the solution" do expect_any_instance_of(described_class).not_to receive(:read_solution) subject end end context "when plugin file exists and solution file does not" do let(:plugin_file_exists) { true } it "should not attempt to read the solution" do expect_any_instance_of(described_class).not_to receive(:read_solution) subject end end context "when solution file exists and plugin file does not" do let(:solution_file_exists) { true } it "should not attempt to read the solution" do expect_any_instance_of(described_class).not_to receive(:read_solution) subject end end context "when solution file and plugin file exist" do let(:plugin_file_exists) { true } let(:solution_file_exists) { true } let(:solution_file_contents) { "" } before do allow(solution_file).to receive(:read).and_return(solution_file_contents) allow_any_instance_of(described_class).to receive(:plugin_file_checksum).and_return("VALID") end context "when solution file is empty" do it "should return false" do expect(subject.send(:load)).to be_falsey end end context "when solution file contains invalid checksum" do let(:solution_file_contents) { {checksum: "INVALID", vagrant_version: Vagrant::VERSION}.to_json } it "should return false" do expect(subject.send(:load)).to be_falsey end end context "when solution file contains different Vagrant version" do let(:solution_file_contents) { {checksum: "VALID", vagrant_version: "0.1"}.to_json } it "should return false" do expect(subject.send(:load)).to be_falsey end end context "when solution file contains valid Vagrant version and valid checksum" do let(:solution_file_contents) { {checksum: "VALID", vagrant_version: Vagrant::VERSION, dependencies: file_dependencies}.to_json } let(:file_dependencies) { dependency_list.map{|d| [d.name, d.requirements_list]} } let(:dependency_list) { [] } it "should return true" do expect(subject.send(:load)).to be_truthy end it "should be valid" do expect(subject).to be_valid end context "when solution file contains dependency list" do let(:dependency_list) { [ Gem::Dependency.new("dep1", "> 0"), Gem::Dependency.new("dep2", "< 3") ] } it "should be valid" do expect(subject).to be_valid end it "should convert list into dependency requests" do subject.dependency_list.each do |d| expect(d).to be_a(Gem::Resolver::DependencyRequest) end end it "should include defined dependencies" do expect(subject.dependency_list.first).to eq(dependency_list.first) expect(subject.dependency_list.last).to eq(dependency_list.last) end it "should freeze the dependency list" do expect(subject.dependency_list).to be_frozen end end end end end describe "#read_solution" do let(:solution_file_contents) { "" } let(:plugin_file_path) { "PLUGIN_FILE_PATH" } let(:solution_file_path) { "SOLUTION_FILE_PATH" } let(:plugin_file) { double("plugin-file") } let(:solution_file) { double("solution-file") } subject do described_class.new(plugin_file: plugin_file_path, solution_file: solution_file_path) end before do allow(Pathname).to receive(:new).with(plugin_file_path).and_return(plugin_file) allow(Pathname).to receive(:new).with(solution_file_path).and_return(solution_file) allow(plugin_file).to receive(:exist?).and_return(false) allow(solution_file).to receive(:exist?).and_return(false) allow(solution_file).to receive(:read).and_return(solution_file_contents) end it "should return nil when file contents are empty" do expect(subject.send(:read_solution)).to be_nil end context "when file contents are hash" do let(:solution_file_contents) { {checksum: "VALID"}.to_json } it "should return a hash" do expect(subject.send(:read_solution)).to be_a(Hash) end it "should return a hash with indifferent access" do expect(subject.send(:read_solution)).to be_a(Vagrant::Util::HashWithIndifferentAccess) end end context "when file contents are array" do let(:solution_file_contents) { ["test"].to_json } it "should return a hash" do expect(subject.send(:read_solution)).to be_a(Hash) end it "should return a hash with indifferent access" do expect(subject.send(:read_solution)).to be_a(Vagrant::Util::HashWithIndifferentAccess) end end context "when file contents are null" do let(:solution_file_contents) { "null" } it "should return nil" do expect(subject.send(:read_solution)).to be_nil end end context "when file contents are invalid" do let(:solution_file_contents) { "{2dfwef" } it "should return nil" do expect(subject.send(:read_solution)).to be_nil end end end end describe Vagrant::Bundler do include_context "unit" let(:iso_env) { isolated_environment } let(:env) { iso_env.create_vagrant_env } let(:tmpdir) { @v_tmpdir ||= Pathname.new(Dir.mktmpdir("vagrant-bundler-test")) } before do @tmpdir = Dir.mktmpdir("vagrant-bundler-test") @vh = ENV["VAGRANT_HOME"] ENV["VAGRANT_HOME"] = @tmpdir end after do ENV["VAGRANT_HOME"] = @vh FileUtils.rm_rf(@tmpdir) FileUtils.rm_rf(@v_tmpdir) if @v_tmpdir end it "should isolate gem path based on Ruby version" do expect(subject.plugin_gem_path.to_s).to end_with(RUBY_VERSION) end it "should not have an env_plugin_gem_path by default" do expect(subject.env_plugin_gem_path).to be_nil end describe "#initialize" do it "should automatically set the plugin gem path" do expect(subject.plugin_gem_path).not_to be_nil end it "should add current ruby version to plugin gem path suffix" do expect(subject.plugin_gem_path.to_s).to end_with(RUBY_VERSION) end it "should freeze the plugin gem path" do expect(subject.plugin_gem_path).to be_frozen end end describe "#environment_path=" do it "should error if not given Pathname" do expect { subject.environment_path = :value }. to raise_error(TypeError) end context "when set with Pathname" do let(:env_path) { Pathname.new("/dev/null") } before { subject.environment_path = env_path } it "should set the environment_data_path" do expect(subject.environment_data_path).to eq(env_path) end it "should set the env_plugin_gem_path" do expect(subject.env_plugin_gem_path).not_to be_nil end it "should suffix current ruby version to env_plugin_gem_path" do expect(subject.env_plugin_gem_path.to_s).to end_with(RUBY_VERSION) end it "should base env_plugin_gem_path on environment_path value" do expect(subject.env_plugin_gem_path.to_s).to start_with(env_path.to_s) end it "should freeze the env_plugin_gem_path" do expect(subject.env_plugin_gem_path).to be_frozen end end end describe "#load_solution_file" do let(:local_opt) { nil } let(:global_opt) { nil } let(:options) { {local: local_opt, global: global_opt} } it "should return nil when local and global options are blank" do expect(subject.load_solution_file(options)).to be_nil end context "when environment data path is set" do let(:env_path) { "/dev/null" } before { subject.environment_path = Pathname.new(env_path) } context "when local option is set" do let(:local_opt) { tmpdir + "local" } it "should return a SolutionFile instance" do expect(subject.load_solution_file(options)).to be_a(Vagrant::Bundler::SolutionFile) end it "should be located in the environment data path" do file = subject.load_solution_file(options) expect(file.solution_file.to_s).to start_with(env_path) end it "should have a local.sol solution file" do file = subject.load_solution_file(options) expect(file.solution_file.to_s).to end_with("local.sol") end it "should have plugin file set to local value" do file = subject.load_solution_file(options) expect(file.plugin_file.to_s).to eq(local_opt.to_s) end end context "when global option is set" do let(:global_opt) { tmpdir + "global" } it "should return a SolutionFile instance" do expect(subject.load_solution_file(options)).to be_a(Vagrant::Bundler::SolutionFile) end it "should be located in the environment data path" do file = subject.load_solution_file(options) expect(file.solution_file.to_s).to start_with(env_path) end it "should have a global.sol solution file" do file = subject.load_solution_file(options) expect(file.solution_file.to_s).to end_with("global.sol") end it "should have plugin file set to global value" do file = subject.load_solution_file(options) expect(file.plugin_file.to_s).to eq(global_opt.to_s) end end context "when local and global option is set" do let(:global_opt) { tmpdir + "global" } let(:local_opt) { tmpdir + "local" } it "should return nil" do expect(subject.load_solution_file(options)).to be_nil end end end context "when environment data path is unset" do context "when local option is set" do let(:local_opt) { tmpdir + "local" } it "should return nil" do expect(subject.load_solution_file(options)).to be_nil end end context "when global option is set" do let(:global_opt) { tmpdir + "global" } it "should return a SolutionFile instance" do expect(subject.load_solution_file(options)).to be_a(Vagrant::Bundler::SolutionFile) end it "should be located in the vagrant user data path" do file = subject.load_solution_file(options) expect(file.solution_file.to_s).to start_with(Vagrant.user_data_path.to_s) end it "should have a global.sol solution file" do file = subject.load_solution_file(options) expect(file.solution_file.to_s).to end_with("global.sol") end it "should have plugin file set to global value" do file = subject.load_solution_file(options) expect(file.plugin_file.to_s).to eq(global_opt.to_s) end end end end describe "#deinit" do it "should provide method for backwards compatibility" do subject.deinit end end describe "DEFAULT_GEM_SOURCES" do it "should list hashicorp gemstore first" do expect(described_class.const_get(:DEFAULT_GEM_SOURCES).first).to eq( described_class.const_get(:HASHICORP_GEMSTORE)) end end describe "#init!" do context "Gem.sources" do before { Gem.sources.clear Gem.sources << "https://rubygems.org/" } it "should add hashicorp gem store" do subject.init!([]) expect(Gem.sources).to include(described_class.const_get(:HASHICORP_GEMSTORE)) end it "should add hashicorp gem store to start of sources list" do subject.init!([]) expect(Gem.sources.sources.first.uri.to_s).to eq(described_class.const_get(:HASHICORP_GEMSTORE)) end end context "multiple specs" do let(:solution_file) { double('solution_file') } let(:vagrant_set) { double('vagrant_set') } before do allow(subject).to receive(:load_solution_file).and_return(solution_file) allow(subject).to receive(:generate_vagrant_set).and_return(vagrant_set) allow(solution_file).to receive(:valid?).and_return(true) end it "should activate spec of deps already loaded" do spec = Gem.loaded_specs.first deps = [spec[0]] specs = [spec[1].dup, spec[1].dup] specs[0].version = Gem::Version::new('0.0.1') # make sure haven't accidentally modified both expect(specs[0].version).to_not eq(specs[1].version) expect(solution_file).to receive(:dependency_list).and_return(deps) expect(vagrant_set).to receive(:find_all).and_return(specs) expect(subject).to receive(:activate_solution) do |activate_specs| expect(activate_specs.length()).to eq(1) expect(activate_specs[0].full_spec()).to eq(specs[1]) end subject.init!([]) end end end describe "#install" do let(:plugins){ {"my-plugin" => {"gem_version" => "> 0"}} } it "should pass plugin information hash to internal install" do expect(subject).to receive(:internal_install).with(plugins, any_args) subject.install(plugins) end it "should not include any update plugins" do expect(subject).to receive(:internal_install).with(anything, nil, any_args) subject.install(plugins) end it "should flag local when local is true" do expect(subject).to receive(:internal_install).with(any_args, env_local: true) subject.install(plugins, true) end it "should not flag local when local is not set" do expect(subject).to receive(:internal_install).with(any_args, env_local: false) subject.install(plugins) end end describe "#install_local" do let(:plugin_source){ double("plugin_source", spec: plugin_spec) } let(:plugin_spec){ double("plugin_spec", name: plugin_name, version: plugin_version) } let(:plugin_name){ "PLUGIN_NAME" } let(:plugin_version){ "1.0.0" } let(:plugin_path){ "PLUGIN_PATH" } let(:sources){ "SOURCES" } before do allow(Gem::Source::SpecificFile).to receive(:new).and_return(plugin_source) allow(subject).to receive(:internal_install) end it "should return plugin gem specification" do expect(subject.install_local(plugin_path)).to eq(plugin_spec) end it "should set custom sources" do expect(subject).to receive(:internal_install) do |info, update, opts| expect(info[plugin_name]["sources"]).to eq(sources) end subject.install_local(plugin_path, sources: sources) end it "should not set the update parameter" do expect(subject).to receive(:internal_install) do |info, update, opts| expect(update).to be_nil end subject.install_local(plugin_path) end it "should not set plugin as environment local by default" do expect(subject).to receive(:internal_install) do |info, update, opts| expect(opts[:env_local]).to be_falsey end subject.install_local(plugin_path) end it "should set if plugin is environment local" do expect(subject).to receive(:internal_install) do |info, update, opts| expect(opts[:env_local]).to be_truthy end subject.install_local(plugin_path, env_local: true) end end describe "#update" do let(:plugins){ :plugins } let(:specific){ [] } after{ subject.update(plugins, specific) } it "should mark update as true" do expect(subject).to receive(:internal_install) do |info, update, opts| expect(update).to be_truthy end end context "with specific plugins named" do let(:specific){ ["PLUGIN_NAME"] } it "should set update to specific names" do expect(subject).to receive(:internal_install) do |info, update, opts| expect(update[:gems]).to eq(specific) end end end end describe "#vagrant_internal_specs" do let(:vagrant_spec) { double("vagrant_spec", name: "vagrant", version: Gem::Version.new(Vagrant::VERSION), activated?: vagrant_spec_activated, activate: nil, runtime_dependencies: vagrant_dep_specs) } let(:spec_list) { [] } let(:spec_dirs) { [] } let(:spec_default_dir) { "/dev/null" } let(:dir_spec_list) { [] } let(:vagrant_spec_activated) { true } let(:vagrant_dep_specs) { [] } before do allow(Gem::Specification).to receive(:find) { |&b| vagrant_spec if b.call(vagrant_spec) } allow(Gem::Specification).to receive(:find_all).and_return(spec_list) allow(Gem::Specification).to receive(:dirs).and_return(spec_dirs) allow(Gem::Specification).to receive(:default_specifications_dir).and_return(spec_default_dir) allow(Gem::Specification).to receive(:each_spec).and_return(dir_spec_list) end it "should return an empty list" do expect(subject.send(:vagrant_internal_specs)).to eq([]) end context "when vagrant specification is not activated" do let(:vagrant_spec_activated) { false } it "should activate the specification" do expect(vagrant_spec).to receive(:activate) subject.send(:vagrant_internal_specs) end end context "when vagrant specification is not found" do before { allow(Gem::Specification).to receive(:find).and_return(nil) } it "should raise not found error" do expect { subject.send(:vagrant_internal_specs) }.to raise_error(Vagrant::Errors::SourceSpecNotFound) end end context "when bundler is not defined" do before { expect(Vagrant).to receive(:in_bundler?).and_return(false) } context "when running inside the installer" do before { expect(Vagrant).to receive(:in_installer?).and_return(true) } it "should load gem specification directories" do expect(Gem::Specification).to receive(:dirs).and_return(spec_dirs) subject.send(:vagrant_internal_specs) end context "when checking paths" do let(:spec_dirs) { [double("spec-dir", start_with?: in_user_dir)] } let(:in_user_dir) { true } let(:user_dir) { double("user-dir") } before { allow(Gem).to receive(:user_dir).and_return(user_dir) } it "should check if path is within local user directory" do expect(spec_dirs.first).to receive(:start_with?).with(user_dir).and_return(false) subject.send(:vagrant_internal_specs) end context "when path is not within user directory" do let(:in_user_dir) { false } it "should use path when loading specs" do expect(Gem::Specification).to receive(:each_spec) { |arg| expect(arg).to include(spec_dirs.first) } subject.send(:vagrant_internal_specs) end end end end context "when running outside the installer" do before { expect(Vagrant).to receive(:in_installer?).and_return(false) } it "should not load gem specification directories" do expect(Gem::Specification).not_to receive(:dirs) subject.send(:vagrant_internal_specs) end end end end describe Vagrant::Bundler::PluginSet do let(:name) { "test-gem" } let(:version) { "1.0.0" } let(:directory) { @directory ||= Dir.mktmpdir("vagrant-bundler-test") } after do FileUtils.rm_rf(@directory) if @directory @directory = nil end describe "#add_vendor_gem" do context "when spec file does not exist" do it "should raise a not found error" do expect { subject.add_vendor_gem(name, directory) }.to raise_error(Gem::GemNotFoundException) end end context "when spec file exists" do before do spec = Gem::Specification.new(name, version) File.write(File.join(directory, "#{name}.gemspec"), spec.to_ruby) end it "should load the specification" do expect(subject.add_vendor_gem(name, directory)).to be_a(Gem::Specification) end it "should set the full path in specification" do spec = subject.add_vendor_gem(name, directory) expect(spec.full_gem_path).to eq(directory) end end end describe "#find_all" do let(:request) { Gem::Resolver::DependencyRequest.new(dependency, nil) } let(:dependency) { Gem::Dependency.new("test-gem", requirement) } let(:requirement) { Gem::Requirement.new(version) } context "when specification is not included in set" do it "should return empty array" do expect(subject.find_all(request)).to eq([]) end end context "when specification is included in set" do before do spec = Gem::Specification.new(name, version) File.write(File.join(directory, "#{name}.gemspec"), spec.to_ruby) subject.add_vendor_gem(name, directory) end it "should return a vendor specification instance" do expect(subject.find_all(request).first).to be_a(Gem::Resolver::VendorSpecification) end end end end end