summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md1
-rw-r--r--DOC_CHANGES.md29
-rw-r--r--RELEASE_NOTES.md14
-rw-r--r--lib/chef/provider/package/windows.rb80
-rw-r--r--lib/chef/provider/package/windows/msi.rb69
-rw-r--r--lib/chef/resource/windows_package.rb79
-rw-r--r--lib/chef/resources.rb1
-rw-r--r--lib/chef/win32/api/installer.rb166
-rw-r--r--spec/unit/provider/package/windows/msi_spec.rb60
-rw-r--r--spec/unit/provider/package/windows_spec.rb80
-rw-r--r--spec/unit/resource/windows_package_spec.rb74
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