diff options
author | Salim Alam <salam@chef.io> | 2015-12-08 10:17:11 -0800 |
---|---|---|
committer | Salim Alam <salam@chef.io> | 2015-12-08 10:17:11 -0800 |
commit | 6a9cec48804f097ce55d2934f97ea4409890506a (patch) | |
tree | feb19eedf7151c47fb5db7b831a07cba8b155670 /spec/unit | |
parent | 5982661df7c09bbdadd5d2b8431f2a7bbba05c0d (diff) | |
parent | 3e704d162e3ef5dff9e929eca7c82b48c4d66305 (diff) | |
download | chef-6a9cec48804f097ce55d2934f97ea4409890506a.tar.gz |
Merge pull request #4193 from chef/mwrock/package
adds support for installer types inno, nsis, wise and installshield to windows_package resource
Diffstat (limited to 'spec/unit')
-rw-r--r-- | spec/unit/provider/package/windows/exe_spec.rb | 251 | ||||
-rw-r--r-- | spec/unit/provider/package/windows/msi_spec.rb | 104 | ||||
-rw-r--r-- | spec/unit/provider/package/windows_spec.rb | 253 |
3 files changed, 572 insertions, 36 deletions
diff --git a/spec/unit/provider/package/windows/exe_spec.rb b/spec/unit/provider/package/windows/exe_spec.rb new file mode 100644 index 0000000000..730df5e067 --- /dev/null +++ b/spec/unit/provider/package/windows/exe_spec.rb @@ -0,0 +1,251 @@ +# +# Author:: Matt Wrock <matt@mattwrock.com> +# Copyright:: Copyright (c) 2015 Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'spec_helper' +require 'chef/provider/package/windows/exe' + +unless Chef::Platform.windows? + class Chef + module ReservedNames::Win32 + class File + def version_info + nil + end + end + end + end +end + +describe Chef::Provider::Package::Windows::Exe do + let(:package_name) { "calculator" } + let(:resource_source) { "calculator.exe" } + let(:new_resource) do + new_resource = Chef::Resource::WindowsPackage.new(package_name) + new_resource.source(resource_source) + new_resource + end + let(:uninstall_hash) do + [{ + 'DisplayVersion' => 'outdated', + 'UninstallString' => File.join("uninst_dir", "uninst_file") + }] + end + let(:uninstall_entry) do + entries = [] + uninstall_hash.each do |entry| + entries.push(Chef::Provider::Package::Windows::RegistryUninstallEntry.new('hive', 'key', entry)) + end + entries + end + let(:provider) { Chef::Provider::Package::Windows::Exe.new(new_resource, :nsis, uninstall_entry) } + let(:file_version) { nil } + let(:product_version) { nil } + let(:version_info) { instance_double("Chef::ReservedNames::Win32::File::Version_info", FileVersion: file_version, ProductVersion: product_version) } + + before(:each) do + allow(Chef::ReservedNames::Win32::File).to receive(:version_info).and_return(version_info) + allow(::File).to receive(:exist?).with(Chef::Util::PathHelper.canonical_path(resource_source, false)).and_return(true) + end + + it "responds to shell_out!" do + expect(provider).to respond_to(:shell_out!) + end + + describe "expand_options" do + it "returns an empty string if passed no options" do + expect(provider.expand_options(nil)).to eql "" + end + + it "returns a string with a leading space if passed options" do + expect(provider.expand_options("--train nope --town no_way")).to eql(" --train nope --town no_way") + end + end + + describe "installed_version" do + it "returns the installed version" do + expect(provider.installed_version).to eql(["outdated"]) + end + + context "no versions installed" do + let(:uninstall_hash) { [] } + + it "returns the installed version" do + expect(provider.installed_version).to eql(nil) + end + end + end + + describe "package_version" do + before { new_resource.version(nil) } + + context "source file does not exist" do + before do + allow(::File).to receive(:exist?).with(Chef::Util::PathHelper.canonical_path(resource_source, false)).and_return(false) + end + + it "returns nil" do + expect(provider.package_version).to eql(nil) + end + end + + context "file version is empty" do + let(:file_version) { '' } + + it "returns nil" do + expect(provider.package_version).to eql(nil) + end + + it "returns the version of a package if given" do + new_resource.version('v55555') + expect(provider.package_version).to eql('v55555') + end + end + + context "both file and product version are in installer" do + let(:file_version) { '1.1.1' } + let(:product_version) { '1.1' } + + it "returns the file version" do + expect(provider.package_version).to eql('1.1.1') + end + + it "returns the version of a package if given" do + new_resource.version('v55555') + expect(provider.package_version).to eql('v55555') + end + end + + context "only file version is in installer" do + let(:file_version) { '1.1.1' } + + it "returns the file version" do + expect(provider.package_version).to eql('1.1.1') + end + + it "returns the version of a package if given" do + new_resource.version('v55555') + expect(provider.package_version).to eql('v55555') + end + end + + context "only product version is in installer" do + let(:product_version) { '1.1' } + + it "returns the product version" do + expect(provider.package_version).to eql('1.1') + end + + it "returns the version of a package if given" do + new_resource.version('v55555') + expect(provider.package_version).to eql('v55555') + end + end + + context "no version info is in installer" do + let(:file_version) { nil } + let(:product_version) { nil } + + it "returns the version of a package" do + new_resource.version('v55555') + expect(provider.package_version).to eql('v55555') + end + end + + context "no version info is in installer and none in attribute" do + it "returns the version of a package" do + expect(provider.package_version).to eql(nil) + end + end + end + + describe "remove_package" do + context "no version given and one package installed" do + it "removes installed package" do + expect(provider).to receive(:shell_out!).with(/start \"\" \/wait \/d\"uninst_dir\" uninst_file \/S \/NCRC & exit %%%%ERRORLEVEL%%%%/, kind_of(Hash)) + provider.remove_package + end + end + + context "several packages installed" do + let(:uninstall_hash) do + [ + { + 'DisplayVersion' => 'v1', + 'UninstallString' => File.join("uninst_dir1", "uninst_file1") + }, + { + 'DisplayVersion' => 'v2', + 'UninstallString' => File.join("uninst_dir2", "uninst_file2") + } + ] + end + + context "version given and installed" do + it "removes given version" do + new_resource.version('v2') + expect(provider).to receive(:shell_out!).with(/start \"\" \/wait \/d\"uninst_dir2\" uninst_file2 \/S \/NCRC & exit %%%%ERRORLEVEL%%%%/, kind_of(Hash)) + provider.remove_package + end + end + + context "no version given" do + it "removes both versions" do + expect(provider).to receive(:shell_out!).with(/start \"\" \/wait \/d\"uninst_dir1\" uninst_file1 \/S \/NCRC & exit %%%%ERRORLEVEL%%%%/, kind_of(Hash)) + expect(provider).to receive(:shell_out!).with(/start \"\" \/wait \/d\"uninst_dir2\" uninst_file2 \/S \/NCRC & exit %%%%ERRORLEVEL%%%%/, kind_of(Hash)) + provider.remove_package + end + end + end + end + + context "installs nsis installer" do + let(:provider) { Chef::Provider::Package::Windows::Exe.new(new_resource, :nsis, uninstall_entry) } + + it "calls installer with the correct flags" do + expect(provider).to receive(:shell_out!).with(/start \"\" \/wait \"#{Regexp.quote(new_resource.source)}\" \/S \/NCRC & exit %%%%ERRORLEVEL%%%%/, kind_of(Hash)) + provider.install_package + end + end + + context "installs installshield installer" do + let(:provider) { Chef::Provider::Package::Windows::Exe.new(new_resource, :installshield, uninstall_entry) } + + it "calls installer with the correct flags" do + expect(provider).to receive(:shell_out!).with(/start \"\" \/wait \"#{Regexp.quote(new_resource.source)}\" \/s \/sms & exit %%%%ERRORLEVEL%%%%/, kind_of(Hash)) + provider.install_package + end + end + + context "installs inno installer" do + let(:provider) { Chef::Provider::Package::Windows::Exe.new(new_resource, :inno, uninstall_entry) } + + it "calls installer with the correct flags" do + expect(provider).to receive(:shell_out!).with(/start \"\" \/wait \"#{Regexp.quote(new_resource.source)}\" \/VERYSILENT \/SUPPRESSMSGBOXES \/NORESTART & exit %%%%ERRORLEVEL%%%%/, kind_of(Hash)) + provider.install_package + end + end + + context "installs wise installer" do + let(:provider) { Chef::Provider::Package::Windows::Exe.new(new_resource, :wise, uninstall_entry) } + + it "calls installer with the correct flags" do + expect(provider).to receive(:shell_out!).with(/start \"\" \/wait \"#{Regexp.quote(new_resource.source)}\" \/s & exit %%%%ERRORLEVEL%%%%/, kind_of(Hash)) + provider.install_package + end + end +end diff --git a/spec/unit/provider/package/windows/msi_spec.rb b/spec/unit/provider/package/windows/msi_spec.rb index bef202847f..9377dcaad9 100644 --- a/spec/unit/provider/package/windows/msi_spec.rb +++ b/spec/unit/provider/package/windows/msi_spec.rb @@ -17,17 +17,37 @@ # require 'spec_helper' +require 'chef/provider/package/windows/msi' describe Chef::Provider::Package::Windows::MSI do let(:node) { double('Chef::Node') } let(:events) { double('Chef::Events').as_null_object } # mock all the methods let(:run_context) { double('Chef::RunContext', :node => node, :events => events) } - let(:new_resource) { Chef::Resource::WindowsPackage.new("calculator.msi") } - let(:provider) { Chef::Provider::Package::Windows::MSI.new(new_resource) } - - before(:each) do - stub_const("File::ALT_SEPARATOR", "\\") - allow(::File).to receive(:absolute_path).with("calculator.msi").and_return("calculator.msi") + let(:package_name) { "calculator" } + let(:resource_source) { "calculator.msi" } + let(:resource_version) { nil } + let(:new_resource) do + new_resource = Chef::Resource::WindowsPackage.new(package_name) + new_resource.source(resource_source) + new_resource.version(resource_version) + new_resource + end + let(:uninstall_hash) do + [{ + 'DisplayVersion' => 'outdated', + 'UninstallString' => "MsiExec.exe /X{guid}" + }] + end + let(:uninstall_entry) do + entries = [] + uninstall_hash.each do |entry| + entries.push(Chef::Provider::Package::Windows::RegistryUninstallEntry.new('hive', 'key', entry)) + end + entries + end + let(:provider) { Chef::Provider::Package::Windows::MSI.new(new_resource, uninstall_entry) } + before do + allow(::File).to receive(:exist?).with(Chef::Util::PathHelper.canonical_path(resource_source, false)).and_return(true) end it "responds to shell_out!" do @@ -50,6 +70,11 @@ describe Chef::Provider::Package::Windows::MSI do allow(provider).to receive(:get_installed_version).with("{23170F69-40C1-2702-0920-000001000000}").and_return("3.14159.1337.42") expect(provider.installed_version).to eql("3.14159.1337.42") end + + it "returns the installed version in the registry when install file not present" do + allow(::File).to receive(:exist?).with(Chef::Util::PathHelper.canonical_path(resource_source, false)).and_return(false) + expect(provider.installed_version).to eql(["outdated"]) + end end describe "package_version" do @@ -57,19 +82,78 @@ describe Chef::Provider::Package::Windows::MSI do allow(provider).to receive(:get_product_property).with(/calculator.msi$/, "ProductVersion").and_return(42) expect(provider.package_version).to eql(42) end + + context "version is explicitly provided" do + let(:resource_version) { "given_version" } + + it "returns the given version" do + expect(provider.package_version).to eql("given_version") + end + end + + context "no source or version is given" do + before do + allow(::File).to receive(:exist?).with(Chef::Util::PathHelper.canonical_path(resource_source, false)).and_return(false) + end + + it "returns nil" do + expect(provider.package_version).to eql(nil) + end + end end describe "install_package" do it "calls msiexec /qn /i" do - expect(provider).to receive(:shell_out!).with(/msiexec \/qn \/i \"calculator.msi\"/, kind_of(Hash)) - provider.install_package("unused", "unused") + expect(provider).to receive(:shell_out!).with(/msiexec \/qn \/i \"#{Regexp.quote(new_resource.source)}\"/, kind_of(Hash)) + provider.install_package end end describe "remove_package" do it "calls msiexec /qn /x" do - expect(provider).to receive(:shell_out!).with(/msiexec \/qn \/x \"calculator.msi\"/, kind_of(Hash)) - provider.remove_package("unused", "unused") + expect(provider).to receive(:shell_out!).with(/msiexec \/qn \/x \"#{Regexp.quote(new_resource.source)}\"/, kind_of(Hash)) + provider.remove_package + end + + context "no source is provided" do + before do + allow(::File).to receive(:exist?).with(Chef::Util::PathHelper.canonical_path(resource_source, false)).and_return(false) + end + + it "removes installed package" do + expect(provider).to receive(:shell_out!).with(/MsiExec.exe \/X{guid} \/Q/, kind_of(Hash)) + provider.remove_package + end + + context "there are multiple installs" do + let(:uninstall_hash) do + [ + { + 'DisplayVersion' => 'outdated', + 'UninstallString' => "MsiExec.exe /X{guid}" + }, + { + 'DisplayVersion' => 'really_outdated', + 'UninstallString' => "MsiExec.exe /X{guid2}" + } + ] + end + + it "removes both installed package" do + expect(provider).to receive(:shell_out!).with(/MsiExec.exe \/X{guid} \/Q/, kind_of(Hash)) + expect(provider).to receive(:shell_out!).with(/MsiExec.exe \/X{guid2} \/Q/, kind_of(Hash)) + provider.remove_package + end + end + + context "custom options includes /Q" do + before { new_resource.options("/Q") } + + it "does not duplicate quiet switch" do + expect(provider).to receive(:shell_out!).with(/MsiExec.exe \/X{guid} \/Q/, kind_of(Hash)) + provider.remove_package + end + end end end end diff --git a/spec/unit/provider/package/windows_spec.rb b/spec/unit/provider/package/windows_spec.rb index e5acc87694..c26c446b5b 100644 --- a/spec/unit/provider/package/windows_spec.rb +++ b/spec/unit/provider/package/windows_spec.rb @@ -17,6 +17,8 @@ # require 'spec_helper' +require 'chef/provider/package/windows/exe' +require 'chef/provider/package/windows/msi' describe Chef::Provider::Package::Windows, :windows_only do before(:each) do @@ -28,10 +30,19 @@ describe Chef::Provider::Package::Windows, :windows_only do let(:events) { double('Chef::Events').as_null_object } # mock all the methods let(:run_context) { double('Chef::RunContext', :node => node, :events => events) } let(:resource_source) { 'calculator.msi' } - let(:new_resource) { Chef::Resource::WindowsPackage.new(resource_source) } + let(:resource_name) { 'calculator' } + let(:new_resource) do + new_resource = Chef::Resource::WindowsPackage.new(resource_name) + new_resource.source(resource_source) + new_resource + end let(:provider) { Chef::Provider::Package::Windows.new(new_resource, run_context) } let(:cache_path) { 'c:\\cache\\' } + before(:each) do + allow(::File).to receive(:exist?).with(provider.new_resource.source).and_return(true) + end + describe "load_current_resource" do shared_examples "a local file" do before(:each) do @@ -43,7 +54,7 @@ describe Chef::Provider::Package::Windows, :windows_only do it "creates a current resource with the name of the new resource" do provider.load_current_resource expect(provider.current_resource).to be_a(Chef::Resource::WindowsPackage) - expect(provider.current_resource.name).to eql(resource_source) + expect(provider.current_resource.name).to eql(resource_name) end it "sets the current version if the package is installed" do @@ -76,19 +87,6 @@ describe Chef::Provider::Package::Windows, :windows_only do end it_behaves_like "a local file" end - - context "when remote_file_attributes are provided" do - let (:remote_file_attributes) { {:path => 'C:\\foobar.msi'} } - before(:each) do - new_resource.remote_file_attributes(remote_file_attributes) - end - - it 'should override the attributes of the remote file resource used' do - expect(::File).to receive(:exists?).with(remote_file_attributes[:path]) - provider.load_current_resource - end - - end end context "when source is a local file" do @@ -98,6 +96,7 @@ describe Chef::Provider::Package::Windows, :windows_only do describe "package_provider" do shared_examples "a local file" do + it "checks that the source path is valid" do expect(Chef::Util::PathHelper).to receive(:validate_path) provider.package_provider @@ -108,9 +107,29 @@ describe Chef::Provider::Package::Windows, :windows_only do expect(provider.package_provider).to be_a(Chef::Provider::Package::Windows::MSI) end - it "raises an error if the installer_type is unknown" do - allow(provider).to receive(:installer_type).and_return(:apt_for_windows) - expect { provider.package_provider }.to raise_error + it "sets the package provider to Exe if the the installer type is :inno" do + allow(provider).to receive(:installer_type).and_return(:inno) + expect(provider.package_provider).to be_a(Chef::Provider::Package::Windows::Exe) + end + + it "sets the package provider to Exe if the the installer type is :nsis" do + allow(provider).to receive(:installer_type).and_return(:nsis) + expect(provider.package_provider).to be_a(Chef::Provider::Package::Windows::Exe) + end + + it "sets the package provider to Exe if the the installer type is :wise" do + allow(provider).to receive(:installer_type).and_return(:wise) + expect(provider.package_provider).to be_a(Chef::Provider::Package::Windows::Exe) + end + + it "sets the package provider to Exe if the the installer type is :installshield" do + allow(provider).to receive(:installer_type).and_return(:installshield) + expect(provider.package_provider).to be_a(Chef::Provider::Package::Windows::Exe) + end + + it "defaults to exe if the installer_type is unknown" do + allow(provider).to receive(:installer_type).and_return(nil) + expect(provider.package_provider).to be_a(Chef::Provider::Package::Windows::Exe) end end @@ -146,20 +165,202 @@ describe Chef::Provider::Package::Windows, :windows_only do end describe "installer_type" do - it "it returns @installer_type if it is set" do + let(:resource_source) { "microsoft_installer.exe" } + + context "there is no source" do + let(:uninstall_hash) do + [{ + 'DisplayVersion' => 'outdated', + 'UninstallString' => "blah blah" + }] + end + let(:uninstall_key) { "blah" } + let(:uninstall_entry) do + entries = [] + uninstall_hash.each do |entry| + entries.push(Chef::Provider::Package::Windows::RegistryUninstallEntry.new('hive', uninstall_key, entry)) + end + entries + end + + before do + allow(Chef::Provider::Package::Windows::RegistryUninstallEntry).to receive(:find_entries).and_return(uninstall_entry) + allow(::File).to receive(:exist?).with(Chef::Util::PathHelper.canonical_path(resource_source, false)).and_return(false) + end + + context "uninstall string contains MsiExec.exe" do + let(:uninstall_hash) do + [{ + 'DisplayVersion' => 'outdated', + 'UninstallString' => "MsiExec.exe /X{guid}" + }] + end + + it "sets installer_type to MSI" do + expect(provider.installer_type).to eql(:msi) + end + end + + context "uninstall string ends with uninst.exe" do + let(:uninstall_hash) do + [{ + 'DisplayVersion' => 'outdated', + 'UninstallString' => %q{"c:/hfhfheru/uninst.exe"} + }] + end + + it "sets installer_type to NSIS" do + expect(provider.installer_type).to eql(:nsis) + end + end + + context "uninstall key ends in _is1" do + let(:uninstall_key) { "blah_is1" } + + it "sets installer_type to inno" do + expect(provider.installer_type).to eql(:inno) + end + end + + context "eninstall entries is empty" do + before { allow(Chef::Provider::Package::Windows::RegistryUninstallEntry).to receive(:find_entries).and_return([]) } + + it "returns nil" do + expect(provider.installer_type).to eql(nil) + end + end + end + + it "returns @installer_type if it is set" do provider.new_resource.installer_type(:downeaster) expect(provider.installer_type).to eql(:downeaster) end - it "sets installer_type to msi if the source ends in .msi" do - provider.new_resource.source("microsoft_installer.msi") - expect(provider.installer_type).to eql(:msi) + it "sets installer_type to inno if the source contains inno" do + allow(::Kernel).to receive(:open).and_yield(StringIO.new('blah blah inno blah')) + expect(provider.installer_type).to eql(:inno) end - it "raises an error if it cannot determine the installer type" do - provider.new_resource.installer_type(nil) - provider.new_resource.source("tomfoolery.now") - expect { provider.installer_type }.to raise_error(ArgumentError) + it "sets installer_type to wise if the source contains wise" do + allow(::Kernel).to receive(:open).and_yield(StringIO.new('blah blah wise blah')) + expect(provider.installer_type).to eql(:wise) + end + + it "sets installer_type to nsis if the source contains nsis" do + allow(::Kernel).to receive(:open).and_yield(StringIO.new('blah blah nullsoft blah')) + expect(provider.installer_type).to eql(:nsis) + end + + context "source ends in .msi" do + let(:resource_source) { "microsoft_installer.msi" } + + it "sets installer_type to msi" do + expect(provider.installer_type).to eql(:msi) + end + end + + context "the source is setup.exe" do + let(:resource_source) { "setup.exe" } + + it "sets installer_type to installshield" do + allow(::Kernel).to receive(:open).and_yield(StringIO.new('')) + expect(provider.installer_type).to eql(:installshield) + end + end + + context "cannot determine the installer type" do + let(:resource_source) { "tomfoolery.now" } + + it "raises an error" do + allow(::Kernel).to receive(:open).and_yield(StringIO.new('')) + provider.new_resource.installer_type(nil) + expect { provider.installer_type }.to raise_error(Chef::Exceptions::CannotDetermineWindowsInstallerType) + end + end + end + + describe "action_install" do + let(:new_resource) { Chef::Resource::WindowsPackage.new("blah.exe") } + before do + new_resource.installer_type(:inno) + allow_any_instance_of(Chef::Provider::Package::Windows::Exe).to receive(:package_version).and_return(new_resource.version) + end + + context "no version given, discovered or installed" do + it "installs latest" do + expect(provider).to receive(:install_package).with("blah.exe", "latest") + provider.run_action(:install) + end + end + + context "no version given or discovered but package is installed" do + before { allow(provider).to receive(:current_version_array).and_return(["5.5.5"]) } + + it "does not install" do + expect(provider).not_to receive(:install_package) + provider.run_action(:install) + end + end + + context "a version is given and none is installed" do + before { new_resource.version('5.5.5') } + + it "installs given version" do + expect(provider).to receive(:install_package).with("blah.exe", "5.5.5") + provider.run_action(:install) + end + end + + context "a version is given and several are installed" do + context "given version matches an installed version" do + before do + new_resource.version('5.5.5') + allow(provider).to receive(:current_version_array).and_return([ ["5.5.5", "4.3.0", "1.1.1"] ]) + end + + it "does not install" do + expect(provider).not_to receive(:install_package) + provider.run_action(:install) + end + end + + context "given version does not match an installed version" do + before do + new_resource.version('5.5.5') + allow(provider).to receive(:current_version_array).and_return([ ["5.5.0", "4.3.0", "1.1.1"] ]) + end + + it "installs given version" do + expect(provider).to receive(:install_package).with("blah.exe", "5.5.5") + provider.run_action(:install) + end + end + end + + context "a version is given and one is installed" do + context "given version matches installed version" do + before do + new_resource.version('5.5.5') + allow(provider).to receive(:current_version_array).and_return(["5.5.5"]) + end + + it "does not install" do + expect(provider).not_to receive(:install_package) + provider.run_action(:install) + end + end + + context "given version does not match installed version" do + before do + new_resource.version('5.5.5') + allow(provider).to receive(:current_version_array).and_return(["5.5.0"]) + end + + it "installs given version" do + expect(provider).to receive(:install_package).with("blah.exe", "5.5.5") + provider.run_action(:install) + end + end end end end |