# Copyright (c) HashiCorp, Inc. # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../base", __FILE__) require "pathname" require "stringio" require "tempfile" require "vagrant/box_metadata" describe Vagrant::Box, :skip_windows do include_context "unit" let(:environment) { isolated_environment } let(:box_collection) { Vagrant::BoxCollection.new(environment.boxes_dir) } let(:name) { "foo" } let(:provider) { :virtualbox } let(:version) { "1.0" } let(:architecture) { "test-architecture" } let(:directory) { environment.box3("foo", "1.0", :virtualbox) } subject { described_class.new(name, provider, version, directory) } describe '#metadata_url' do subject { super().metadata_url } it { should be_nil } end it "provides the name" do expect(subject.name).to eq(name) end it "provides the provider" do expect(subject.provider).to eq(provider) end it "provides the directory" do expect(subject.directory).to eq(directory) end it "provides the metadata associated with a box" do data = { "foo" => "bar" } # Write the metadata directory.join("metadata.json").open("w") do |f| f.write(JSON.generate(data)) end # Verify the metadata expect { subject.metadata }. to raise_error(Vagrant::Errors::BoxMetadataMissingRequiredFields) end it "provides the metadata associated with a box" do data = { "provider" => "bar" } # Write the metadata directory.join("metadata.json").open("w") do |f| f.write(JSON.generate(data)) end # Verify the metadata expect(subject.metadata).to eq(data) end context "with a metadata URL" do subject do described_class.new( name, provider, version, directory, metadata_url: "foo") end describe '#metadata_url' do subject { super().metadata_url } it { should eq("foo") } end end context "with a corrupt metadata file" do before do directory.join("metadata.json").open("w") do |f| f.write("") end end it "should raise an exception" do expect { subject }. to raise_error(Vagrant::Errors::BoxMetadataCorrupted) end end context "without a metadata file" do before :each do directory.join("metadata.json").delete end it "should raise an exception" do expect { subject }. to raise_error(Vagrant::Errors::BoxMetadataFileNotFound) end end context "#has_update?" do subject do described_class.new( name, provider, version, directory, metadata_url: "foo") end it "raises an exception if no metadata_url is set" do subject = described_class.new( name, provider, version, directory) expect { subject.has_update?("> 0") }. to raise_error(Vagrant::Errors::BoxUpdateNoMetadata) end it "returns nil if there is no update" do metadata = Vagrant::BoxMetadata.new(StringIO.new(<<-RAW)) { "name": "foo", "versions": [ { "version": "1.0" } ] } RAW allow(subject).to receive(:load_metadata).and_return(metadata) expect(subject.has_update?).to be_nil end it "returns the updated box info if there is an update available" do metadata = Vagrant::BoxMetadata.new(StringIO.new(<<-RAW)) { "name": "foo", "versions": [ { "version": "1.0" }, { "version": "1.1", "providers": [ { "name": "virtualbox", "url": "bar" } ] } ] } RAW allow(subject).to receive(:load_metadata).and_return(metadata) result = subject.has_update? expect(result).to_not be_nil expect(result[0]).to be_kind_of(Vagrant::BoxMetadata) expect(result[1]).to be_kind_of(Vagrant::BoxMetadata::Version) expect(result[2]).to be_kind_of(Vagrant::BoxMetadata::Provider) expect(result[0].name).to eq("foo") expect(result[1].version).to eq("1.1") expect(result[2].url).to eq("bar") end it "returns the updated box info within constraints" do metadata = Vagrant::BoxMetadata.new(StringIO.new(<<-RAW)) { "name": "foo", "versions": [ { "version": "1.0" }, { "version": "1.1", "providers": [ { "name": "virtualbox", "url": "bar" } ] }, { "version": "1.4", "providers": [ { "name": "virtualbox", "url": "bar" } ] } ] } RAW allow(subject).to receive(:load_metadata).and_return(metadata) result = subject.has_update?(">= 1.1, < 1.4") expect(result).to_not be_nil expect(result[0]).to be_kind_of(Vagrant::BoxMetadata) expect(result[1]).to be_kind_of(Vagrant::BoxMetadata::Version) expect(result[2]).to be_kind_of(Vagrant::BoxMetadata::Provider) expect(result[0].name).to eq("foo") expect(result[1].version).to eq("1.1") expect(result[2].url).to eq("bar") end context "with architecture" do subject do described_class.new( name, provider, version, directory, architecture: architecture, metadata_url: "foo" ) end it "raises an exception if no metadata_url is set" do subject = described_class.new( name, provider, version, directory, architecture: architecture, ) expect { subject.has_update?("> 0") }. to raise_error(Vagrant::Errors::BoxUpdateNoMetadata) end it "returns nil if there is no update" do metadata = Vagrant::BoxMetadata.new( { name: "foo", versions: [ { version: "1.0" } ] }.to_json ) allow(subject).to receive(:load_metadata).and_return(metadata) expect(subject.has_update?).to be_nil end it "returns the updated box info if there is an update available" do metadata = Vagrant::BoxMetadata.new( { name: "foo", versions: [ {version: "1.0"}, { version: "1.1", providers: [ { name: "virtualbox", url: "bar", architecture: architecture, } ] } ] }.to_json ) allow(subject).to receive(:load_metadata).and_return(metadata) result = subject.has_update? expect(result).to_not be_nil expect(result[0]).to be_kind_of(Vagrant::BoxMetadata) expect(result[1]).to be_kind_of(Vagrant::BoxMetadata::Version) expect(result[2]).to be_kind_of(Vagrant::BoxMetadata::Provider) expect(result[0].name).to eq("foo") expect(result[1].version).to eq("1.1") expect(result[2].url).to eq("bar") end it "returns nil if update does not support architecture" do metadata = Vagrant::BoxMetadata.new( { name: "foo", versions: [ {version: "1.0"}, { version: "1.1", providers: [ { name: "virtualbox", url: "bar", architecture: "other-architecture", } ] } ] }.to_json ) allow(subject).to receive(:load_metadata).and_return(metadata) result = subject.has_update? expect(result).to be_nil end it "returns the updated box info within constraints" do metadata = Vagrant::BoxMetadata.new( { name: "foo", versions: [ { version: "1.0", }, { version: "1.1", providers: [ { name: "virtualbox", url: "bar", architecture: architecture }, ] }, { version: "1.2", providers: [ { name: "virtualbox", url: "bar", architecture: "other-architecture", }, ] }, { version: "1.4", providers: [ { name: "virtualbox", url: "bar", architecture: architecture } ] } ] }.to_json ) allow(subject).to receive(:load_metadata).and_return(metadata) result = subject.has_update?(">= 1.1, < 1.4") expect(result).to_not be_nil expect(result[0]).to be_kind_of(Vagrant::BoxMetadata) expect(result[1]).to be_kind_of(Vagrant::BoxMetadata::Version) expect(result[2]).to be_kind_of(Vagrant::BoxMetadata::Provider) expect(result[0].name).to eq("foo") expect(result[1].version).to eq("1.1") expect(result[2].url).to eq("bar") end end end context "#automatic_update_check_allowed?" do it "should return true on intial check" do expect(subject.automatic_update_check_allowed?).to be_truthy end it "should return false on second check" do expect(subject.automatic_update_check_allowed?).to be_truthy expect(subject.automatic_update_check_allowed?).to be_falsey end it "should use a file to mark last check time" do expect(FileUtils).to receive(:touch) subject.automatic_update_check_allowed? end it "should return true when time since last check is greater than check interval" do subject.automatic_update_check_allowed? stub_const("Vagrant::Box::BOX_UPDATE_CHECK_INTERVAL", -1) expect(subject.automatic_update_check_allowed?).to be_truthy end end context "#in_use?" do let(:index) { [] } def new_entry(name, provider, version, architecture=nil) Vagrant::MachineIndex::Entry.new.tap do |entry| entry.extra_data["box"] = { "name" => name, "provider" => provider, "version" => version, "architecture" => architecture, } end end it "returns nil if the index has no matching entries" do index << new_entry("foo", "bar", "1.0") index << new_entry("foo", "baz", "1.2") expect(subject).to_not be_in_use(index) end it "returns matching entries if they exist" do matching = new_entry(name, provider.to_s, version) index << new_entry("foo", "bar", "1.0") index << matching index << new_entry("foo", "baz", "1.2") expect(subject.in_use?(index)).to eq([matching]) end context "with architecture information" do subject { described_class.new(name, provider, version, directory, architecture: architecture) } it "returns nil if the index has no matching entries" do index << new_entry("foo", "bar", "1.0", "amd64") index << new_entry("foo", "baz", "1.2", "arm64") index << new_entry(name, provider.to_s, version, "random-arch") expect(subject).to_not be_in_use(index) end it "returns matching entries if they exist" do matching = new_entry(name, provider.to_s, version, architecture) index << new_entry("foo", "bar", "1.0", "amd64") index << matching index << new_entry("foo", "baz", "1.2") expect(subject.in_use?(index)).to eq([matching]) end end end context "#load_metadata" do let(:metadata_url) do Tempfile.new("vagrant-test-box-test").tap do |f| f.write(<<-RAW) { "name": "foo", "description": "bar" } RAW f.close end end subject do described_class.new( name, provider, version, directory, metadata_url: metadata_url.path) end after do metadata_url.unlink end it "loads the url and returns the data" do result = subject.load_metadata expect(result.name).to eq("foo") expect(result.description).to eq("bar") end it "raises an error if the download failed" do dl = double("downloader") allow(Vagrant::Util::Downloader).to receive(:new).and_return(dl) expect(dl).to receive(:download!).and_raise( Vagrant::Errors::DownloaderError.new(message: "foo")) expect { subject.load_metadata }. to raise_error(Vagrant::Errors::BoxMetadataDownloadError) end context "box has a hook for adding authentication" do let(:hook){ double("hook") } subject do described_class.new( name, provider, version, directory, metadata_url: metadata_url.path, hook: hook) end it "add authentication headers to the url" do expect(hook).to receive(:call).with(:authenticate_box_downloader, any_args) result = subject.load_metadata expect(result.name).to eq("foo") expect(result.description).to eq("bar") end end end describe "destroying" do it "should destroy an existing box" do # Verify that our "box" exists expect(directory.exist?).to be # Destroy it expect(subject.destroy!).to be # Verify that it is "destroyed" expect(directory.exist?).not_to be end it "should not error destroying a non-existent box" do # Get the subject so that it is instantiated box = subject # Delete the directory directory.rmtree # Destroy it expect(box.destroy!).to be end end describe "repackaging" do let(:scratch) { Dir.mktmpdir("vagrant-test-box-repackaging") } let(:box_output_path) { File.join(scratch, "package.box") } after do FileUtils.rm_rf(scratch) end it "should repackage the box", :bsdtar do test_file_contents = "hello, world!" # Put a file in the box directory to verify it is packaged properly # later. directory.join("test_file").open("w") do |f| f.write(test_file_contents) end # Repackage our box to some temporary directory expect(subject.repackage(box_output_path)).to be(true) # Let's now add this box again under a different name, and then # verify that we get the proper result back. new_box = box_collection.add(box_output_path, "foo2", "1.0") expect(new_box.directory.join("test_file").read).to eq(test_file_contents) end end describe "comparison and ordering" do it "should be equal if the name, provider, version match" do a = described_class.new("a", :foo, "1.0", directory) b = described_class.new("a", :foo, "1.0", directory) expect(a).to eq(b) end it "should not be equal if name doesn't match" do a = described_class.new("a", :foo, "1.0", directory) b = described_class.new("b", :foo, "1.0", directory) expect(a).to_not eq(b) end it "should not be equal if provider doesn't match" do a = described_class.new("a", :foo, "1.0", directory) b = described_class.new("a", :bar, "1.0", directory) expect(a).to_not eq(b) end it "should not be equal if version doesn't match" do a = described_class.new("a", :foo, "1.0", directory) b = described_class.new("a", :foo, "1.1", directory) expect(a).to_not eq(b) end it "should sort them in order of name, version, provider" do a = described_class.new("a", :foo, "1.0", directory) b = described_class.new("a", :foo2, "1.0", directory) c = described_class.new("a", :foo2, "1.1", directory) d = described_class.new("b", :foo2, "1.0", directory) expect([d, c, a, b].sort).to eq([a, b, c, d]) end end end