diff options
author | Bryan McLellan <btm@opscode.com> | 2014-02-27 13:50:40 -0800 |
---|---|---|
committer | Bryan McLellan <btm@loftninjas.org> | 2014-03-27 19:19:34 -0400 |
commit | 9e9cab6a89d9dab4ddf852b88eb522610d89f2d8 (patch) | |
tree | e5799c600477a37331f66e618201a042413e60e1 | |
parent | df07eaf9e8905ca1fce72cf3186b30c600fd7251 (diff) | |
download | chef-9e9cab6a89d9dab4ddf852b88eb522610d89f2d8.tar.gz |
CHEF-5087: Add a Windows Installer package provider
Adds the framework for a windows package provider, which must determine the
correct provider by examining metadata about the source file, or the source
file itself.
Provides FFI based access to the Windows Installer functions to retrieve
metadata from the MSI files and from the Windows product database.
Combines both of these into an MSI package provider.
Continues to work alongside the windows_package LWRP.
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rw-r--r-- | DOC_CHANGES.md | 29 | ||||
-rw-r--r-- | RELEASE_NOTES.md | 14 | ||||
-rw-r--r-- | lib/chef/provider/package/windows.rb | 80 | ||||
-rw-r--r-- | lib/chef/provider/package/windows/msi.rb | 69 | ||||
-rw-r--r-- | lib/chef/resource/windows_package.rb | 79 | ||||
-rw-r--r-- | lib/chef/resources.rb | 1 | ||||
-rw-r--r-- | lib/chef/win32/api/installer.rb | 166 | ||||
-rw-r--r-- | spec/unit/provider/package/windows/msi_spec.rb | 60 | ||||
-rw-r--r-- | spec/unit/provider/package/windows_spec.rb | 80 | ||||
-rw-r--r-- | spec/unit/resource/windows_package_spec.rb | 74 |
11 files changed, 653 insertions, 0 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index a38ae9ffe5..27811f1a77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ which was accidentally removed in 11.0.0. * Don't save the run_list during `node.save` when running with override run list. (CHEF-4443) * Enable Content-Length validation for Chef::HTTP::Simple and fix issues around it. (CHEF-5041, CHEF-5100) +* Windows MSI Package Provider (CHEF-5087) ## Last Release: 11.10.0 (02/06/2014) diff --git a/DOC_CHANGES.md b/DOC_CHANGES.md index f29af23d30..e5f1c2a481 100644 --- a/DOC_CHANGES.md +++ b/DOC_CHANGES.md @@ -140,3 +140,32 @@ The default value of this setting is `false` feature. Enabling this on a client that connects to a 10.X API server will cause client registration to silently fail. Don't do it. +### Windows Installer (MSI) Package Provider + +The windows_package provider installs and removes Windows Installer (MSI) packages. +This provider utilizies the ProductCode extracted from the MSI package to determine +if the package is currently installed. + +You may use the ```package``` resource to use this provider, and you must use the +```package``` resource if you are also using the windows cookbook, which contains +the windows_package LWRP. + +#### Example + +``` +package "7zip" do + action :install + source 'C:\7z920.msi' +end +``` + +#### Actions +* :install +* :remove + +#### Attributes +* source - The location of the package to install. Default value: the ```name``` of the resource. +* options - Additional options that are passed to msiexec. +* installer_type - The type of package being installed. Can be auto-detected. Currently only :msi is supported. +* timeout - The time in seconds allowed for the package to successfully be installed. Defaults to 600 seconds. +* returns - Return codes that signal a successful installation. Defaults to 0. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 5d0f27b738..9856ddf660 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -89,6 +89,20 @@ recipe which is not a dependency of any cookbook specified in the run list will now log a warning with a message describing the problem and solution. In the future, this warning will become an error. +#### Windows MSI Package Provider + +The first windows package provider has been added to core Chef. It supports Windows Installer (MSI) files only, +and maintains idempotency by using the ProductCode from inside the MSI to determine if the products installation state. + +``` +package "install 7zip" do + action :install + source 'c:\downloads\7zip.msi' +end +``` + +You can continue to use the windows_package LWRP from the windows cookbook alongside this provider. + #### reboot_pending? We have added a ```reboot_pending?``` method to the recipe DSL. This method returns true or false if the operating system diff --git a/lib/chef/provider/package/windows.rb b/lib/chef/provider/package/windows.rb new file mode 100644 index 0000000000..be1de0b969 --- /dev/null +++ b/lib/chef/provider/package/windows.rb @@ -0,0 +1,80 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# Copyright:: Copyright (c) 2014 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 'chef/resource/windows_package' +require 'chef/provider/package' + +class Chef + class Provider + class Package + class Windows < Chef::Provider::Package + + # Depending on the installer, we may need to examine installer_type or + # source attributes, or search for text strings in the installer file + # binary to determine the installer type for the user. Since the file + # must be on disk to do so, we have to make this choice in the provider. + require 'chef/provider/package/windows/msi.rb' + + # load_current_resource is run in Chef::Provider#run_action when not in whyrun_mode? + def load_current_resource + @current_resource = Chef::Resource::WindowsPackage.new(@new_resource.name) + @current_resource.version(package_provider.installed_version) + @new_resource.version(package_provider.package_version) + @current_resource + end + + def package_provider + @package_provider ||= begin + case installer_type + when :msi + Chef::Provider::Package::Windows::MSI.new(@new_resource) + else + raise "Unable to find a Chef::Provider::Package::Windows provider for installer_type '#{installer_type}'" + end + end + end + + def installer_type + @installer_type ||= begin + if @new_resource.installer_type + @new_resource.installer_type + else + file_extension = ::File.basename(@new_resource.source).split(".").last.downcase + + if file_extension == "msi" + :msi + else + raise ArgumentError, "Installer type for Windows Package '#{@new_resource.name}' not specified and cannot be determined from file extension '#{file_extension}'" + end + end + end + end + + # Chef::Provider::Package action_install + action_remove call install_package + remove_package + # Pass those calls to the correct sub-provider + def install_package(name, version) + package_provider.install_package(name, version) + end + + def remove_package(name, version) + package_provider.remove_package(name, version) + end + end + end + end +end diff --git a/lib/chef/provider/package/windows/msi.rb b/lib/chef/provider/package/windows/msi.rb new file mode 100644 index 0000000000..0764a15901 --- /dev/null +++ b/lib/chef/provider/package/windows/msi.rb @@ -0,0 +1,69 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# Copyright:: Copyright (c) 2014 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. +# + +# TODO: Allow @new_resource.source to be a Product Code as a GUID for uninstall / network install + +require 'chef/mixin/shell_out' +require 'chef/win32/api/installer' + +class Chef + class Provider + class Package + class Windows + class MSI + include Chef::ReservedNames::Win32::API::Installer + include Chef::Mixin::ShellOut + + def initialize(resource) + @new_resource = resource + end + + # From Chef::Provider::Package + def expand_options(options) + options ? " #{options}" : "" + end + + # Returns a version if the package is installed or nil if it is not. + def installed_version + Chef::Log.debug("#{@new_resource} getting product code for package at #{@new_resource.source}") + product_code = get_product_property(@new_resource.source, "ProductCode") + Chef::Log.debug("#{@new_resource} checking package status and verion for #{product_code}") + get_installed_version(product_code) + end + + def package_version + Chef::Log.debug("#{@new_resource} getting product version for package at #{@new_resource.source}") + get_product_property(@new_resource.source, "ProductVersion") + end + + def install_package(name, version) + # We could use MsiConfigureProduct here, but we'll start off with msiexec + Chef::Log.debug("#{@new_resource} installing MSI package '#{@new_resource.source}'") + shell_out!("msiexec /qn /i \"#{@new_resource.source}\" #{expand_options(@new_resource.options)}", {:timeout => @new_resource.timeout, :returns => @new_resource.returns}) + end + + def remove_package(name, version) + # We could use MsiConfigureProduct here, but we'll start off with msiexec + Chef::Log.debug("#{@new_resource} removing MSI package '#{@new_resource.source}'") + shell_out!("msiexec /qn /x \"#{@new_resource.source}\" #{expand_options(@new_resource.options)}", {:timeout => @new_resource.timeout, :returns => @new_resource.returns}) + end + end + end + end + end +end diff --git a/lib/chef/resource/windows_package.rb b/lib/chef/resource/windows_package.rb new file mode 100644 index 0000000000..ff80b47115 --- /dev/null +++ b/lib/chef/resource/windows_package.rb @@ -0,0 +1,79 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# Copyright:: Copyright (c) 2014 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 'chef/resource/package' +require 'chef/provider/package/windows' +require 'chef/win32/error' + +class Chef + class Resource + class WindowsPackage < Chef::Resource::Package + + provides :package, :on_platforms => ["windows"] + + def initialize(name, run_context=nil) + super + @allowed_actions = [ :install, :remove ] + @provider = Chef::Provider::Package::Windows + @resource_name = :windows_package + @source ||= source(@package_name) + + # Unique to this resource + @installer_type = nil + @timeout = 600 + # In the past we accepted return code 127 for an unknown reason and 42 because of a bug + @returns = [ 0 ] + end + + def installer_type(arg=nil) + set_or_return( + :installer_type, + arg, + :kind_of => [ String ] + ) + end + + def timeout(arg=nil) + set_or_return( + :timeout, + arg, + :kind_of => [ String, Integer ] + ) + end + + def returns(arg=nil) + set_or_return( + :returns, + arg, + :kind_of => [ String, Integer, Array ] + ) + end + + def source(arg=nil) + if arg == nil && self.instance_variable_defined?(:@source) == true + @source + else + raise ArgumentError, "Bad type for WindowsPackage resource, use a String" unless arg.is_a?(String) + Chef::Log.debug("#{package_name}: sanitizing source path '#{arg}'") + @source = ::File.absolute_path(arg).gsub(::File::SEPARATOR, ::File::ALT_SEPARATOR) + end + end + end + end +end + diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb index 76adb6f1e1..711becef8c 100644 --- a/lib/chef/resources.rb +++ b/lib/chef/resources.rb @@ -69,6 +69,7 @@ require 'chef/resource/template' require 'chef/resource/timestamped_deploy' require 'chef/resource/user' require 'chef/resource/whyrun_safe_ruby_block' +require 'chef/resource/windows_package' require 'chef/resource/yum_package' require 'chef/resource/lwrp_base' require 'chef/resource/bff_package' diff --git a/lib/chef/win32/api/installer.rb b/lib/chef/win32/api/installer.rb new file mode 100644 index 0000000000..745802d260 --- /dev/null +++ b/lib/chef/win32/api/installer.rb @@ -0,0 +1,166 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# Copyright:: Copyright (c) 2014 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 'chef/exceptions' +require 'chef/win32/api' +require 'chef/win32/error' +require 'pathname' + +class Chef + module ReservedNames::Win32 + module API + module Installer + extend Chef::ReservedNames::Win32 + extend Chef::ReservedNames::Win32::API + + ############################################### + # Win32 API Constants + ############################################### + + + ############################################### + # Win32 API Bindings + ############################################### + + ffi_lib 'msi' + +=begin +UINT MsiOpenPackage( + _In_ LPCTSTR szPackagePath, + _Out_ MSIHANDLE *hProduct +); +=end + safe_attach_function :msi_open_package, :MsiOpenPackageExA, [ :string, :int, :pointer ], :int + +=begin +UINT MsiGetProductProperty( + _In_ MSIHANDLE hProduct, + _In_ LPCTSTR szProperty, + _Out_ LPTSTR lpValueBuf, + _Inout_ DWORD *pcchValueBuf +); +=end + safe_attach_function :msi_get_product_property, :MsiGetProductPropertyA, [ :pointer, :pointer, :pointer, :pointer ], :int + +=begin +UINT MsiGetProductInfo( + _In_ LPCTSTR szProduct, + _In_ LPCTSTR szProperty, + _Out_ LPTSTR lpValueBuf, + _Inout_ DWORD *pcchValueBuf +); +=end + safe_attach_function :msi_get_product_info, :MsiGetProductInfoA, [ :pointer, :pointer, :pointer, :pointer ], :int + +=begin +UINT MsiCloseHandle( + _In_ MSIHANDLE hAny +); +=end + safe_attach_function :msi_close_handle, :MsiCloseHandle, [ :pointer ], :int + + ############################################### + # Helpers + ############################################### + + # Opens a Microsoft Installer (MSI) file from an absolute path and returns the specified property + def get_product_property(package_path, property_name) + pkg_ptr = open_package(package_path) + + buffer = 0.chr + buffer_length = FFI::Buffer.new(:long).write_long(0) + + # Fetch the length of the property + status = msi_get_product_property(pkg_ptr.read_pointer, property_name, buffer, buffer_length) + + # We expect error ERROR_MORE_DATA (234) here because we passed a buffer length of 0 + if status != 234 + msg = "msi_get_product_property: returned unknown error #{status} when retrieving #{property_name}: " + msg << Chef::ReservedNames::Win32::Error.format_message(status) + raise Chef::Exceptions::Package, msg + end + + buffer_length = FFI::Buffer.new(:long).write_long(buffer_length.read_long + 1) + buffer = 0.chr * buffer_length.read_long + + # Fetch the property + status = msi_get_product_property(pkg_ptr.read_pointer, property_name, buffer, buffer_length) + + if status != 0 + msg = "msi_get_product_property: returned unknown error #{status} when retrieving #{property_name}: " + msg << Chef::ReservedNames::Win32::Error.format_message(status) + raise Chef::Exceptions::Package, msg + end + + msi_close_handle(pkg_ptr.read_pointer) + return buffer + end + + # Opens a Microsoft Installer (MSI) file from an absolute path and returns a pointer to a handle + # Remember to close the handle with msi_close_handle() + def open_package(package_path) + # MsiOpenPackage expects a perfect absolute Windows path to the MSI + raise ArgumentError, "Provided path '#{package_path}' must be an absolute path" unless Pathname.new(package_path).absolute? + + pkg_ptr = FFI::MemoryPointer.new(:pointer, 4) + status = msi_open_package(package_path, 1, pkg_ptr) + case status + when 0 + # success + else + raise Chef::Exceptions::Package, "msi_open_package: unexpected status #{status}: #{Chef::ReservedNames::Win32::Error.format_message(status)}" + end + return pkg_ptr + end + + # All installed product_codes should have a VersionString + # Returns a version if installed, nil if not installed + def get_installed_version(product_code) + version = 0.chr + version_length = FFI::Buffer.new(:long).write_long(0) + + status = msi_get_product_info(product_code, "VersionString", version, version_length) + + return nil if status == 1605 # ERROR_UNKNOWN_PRODUCT (0x645) + + # We expect error ERROR_MORE_DATA (234) here because we passed a buffer length of 0 + if status != 234 + msg = "msi_get_product_info: product code '#{product_code}' returned unknown error #{status} when retrieving VersionString: " + msg << Chef::ReservedNames::Win32::Error.format_message(status) + raise Chef::Exceptions::Package, msg + end + + # We could fetch the product version now that we know the variable length, but we don't need it here. + + version_length = FFI::Buffer.new(:long).write_long(version_length.read_long + 1) + version = 0.chr * version_length.read_long + + status = msi_get_product_info(product_code, "VersionString", version, version_length) + + if status != 0 + msg = "msi_get_product_info: product code '#{product_code}' returned unknown error #{status} when retrieving VersionString: " + msg << Chef::ReservedNames::Win32::Error.format_message(status) + raise Chef::Exceptions::Package, msg + end + + version + end + end + end + end +end diff --git a/spec/unit/provider/package/windows/msi_spec.rb b/spec/unit/provider/package/windows/msi_spec.rb new file mode 100644 index 0000000000..69322a609d --- /dev/null +++ b/spec/unit/provider/package/windows/msi_spec.rb @@ -0,0 +1,60 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# Copyright:: Copyright (c) 2014 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' + +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) } + + 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 + provider.stub(:get_product_property).and_return("{23170F69-40C1-2702-0920-000001000000}") + provider.stub(: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 + end + + describe "package_version" do + it "returns the version of a package" do + provider.stub(:get_product_property).with(/calculator.msi$/, "ProductVersion").and_return(42) + expect(provider.package_version).to eql(42) + end + end + + describe "install_package" do + # calls shell_out! + end + + describe "remove_package" do + # calls shell_out! + end +end diff --git a/spec/unit/provider/package/windows_spec.rb b/spec/unit/provider/package/windows_spec.rb new file mode 100644 index 0000000000..962bf6fddf --- /dev/null +++ b/spec/unit/provider/package/windows_spec.rb @@ -0,0 +1,80 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# Copyright:: Copyright (c) 2014 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' + +describe Chef::Provider::Package::Windows 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.new(new_resource, run_context) } + + describe "load_current_resource" do + before(:each) do + provider.stub(:package_provider).and_return(double('package_provider', + :installed_version => "1.0", :package_version => "2.0")) + end + + 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("calculator.msi") + end + + it "sets the current version if the package is installed" do + provider.load_current_resource + expect(provider.current_resource.version).to eql("1.0") + end + + it "sets the version to be installed" do + provider.load_current_resource + expect(provider.new_resource.version).to eql("2.0") + end + end + + describe "package_provider" do + it "sets the package provider to MSI if the the installer type is :msi" do + provider.stub(:installer_type).and_return(:msi) + expect(provider.package_provider).to be_a(Chef::Provider::Package::Windows::MSI) + end + + it "raises an error if the installer_type is unknown" do + provider.stub(:installer_type).and_return(:apt_for_windows) + expect { provider.package_provider }.to raise_error + end + end + + describe "installer_type" do + it "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) + 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) + end + end +end diff --git a/spec/unit/resource/windows_package_spec.rb b/spec/unit/resource/windows_package_spec.rb new file mode 100644 index 0000000000..23454e97e4 --- /dev/null +++ b/spec/unit/resource/windows_package_spec.rb @@ -0,0 +1,74 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# Copyright:: Copyright (c) 2014 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' + +describe Chef::Resource::WindowsPackage, "initialize" do + + let(:resource) { Chef::Resource::WindowsPackage.new("solitaire.msi") } + + it "returns a Chef::Resource::WindowsPackage" do + expect(resource).to be_a_kind_of(Chef::Resource::WindowsPackage) + end + + it "sets the resource_name to :windows_package" do + expect(resource.resource_name).to eql(:windows_package) + end + + it "sets the provider to Chef::Provider::Package::Windows" do + expect(resource.provider).to eql(Chef::Provider::Package::Windows) + end + + it "supports setting installer_type" do + resource.installer_type("msi") + expect(resource.installer_type).to eql("msi") + end + + # String, Integer + [ "600", 600 ].each do |val| + it "supports setting a timeout as a #{val.class}" do + resource.timeout(val) + expect(resource.timeout).to eql(val) + end + end + + # String, Integer, Array + [ "42", 42, [47, 48, 49] ].each do |val| + it "supports setting an alternate return value as a #{val.class}" do + resource.returns(val) + expect(resource.returns).to eql(val) + end + end + + it "coverts a source to an absolute path" do + ::File.stub(:absolute_path).and_return("c:\\Files\\frost.msi") + resource.source("frost.msi") + expect(resource.source).to eql "c:\\Files\\frost.msi" + end + + it "converts slashes to backslashes in the source path" do + ::File.stub(:absolute_path).and_return("c:\\frost.msi") + resource.source("c:/frost.msi") + expect(resource.source).to eql "c:\\frost.msi" + end + + it "defaults source to the resource name" do + # it's a little late to stub out File.absolute_path + expect(resource.source).to include("solitaire.msi") + end +end |