diff options
Diffstat (limited to 'spec')
275 files changed, 17167 insertions, 4049 deletions
diff --git a/spec/data/big_json_plus_one.json b/spec/data/big_json_plus_one.json deleted file mode 100644 index 8ea4b74644..0000000000 --- a/spec/data/big_json_plus_one.json +++ /dev/null @@ -1,2 +0,0 @@ -{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":"test" -}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}} diff --git a/spec/data/cookbooks/openldap/templates/default/helpers.erb b/spec/data/cookbooks/openldap/templates/default/helpers.erb new file mode 100644 index 0000000000..b973a5287c --- /dev/null +++ b/spec/data/cookbooks/openldap/templates/default/helpers.erb @@ -0,0 +1,14 @@ +<%= @cookbook_name %> +<%= @recipe_name %> +<%= @recipe_line_string %> +<%= @recipe_path %> +<%= @recipe_line %> +<%= @template_name %> +<%= @template_path %> +<%= cookbook_name %> +<%= recipe_name %> +<%= recipe_line_string %> +<%= recipe_path %> +<%= recipe_line %> +<%= template_name %> +<%= template_path %> diff --git a/spec/data/dsc_lcm.pfx b/spec/data/dsc_lcm.pfx Binary files differnew file mode 100644 index 0000000000..3912ed3753 --- /dev/null +++ b/spec/data/dsc_lcm.pfx diff --git a/spec/data/lwrp/providers/buck_passer.rb b/spec/data/lwrp/providers/buck_passer.rb index c56ab94f85..2bbca07bf7 100644 --- a/spec/data/lwrp/providers/buck_passer.rb +++ b/spec/data/lwrp/providers/buck_passer.rb @@ -1,11 +1,28 @@ -provides 'buck_passer' +provides :buck_passer + +def without_deprecation_warnings(&block) + old_treat_deprecation_warnings_as_errors = Chef::Config[:treat_deprecation_warnings_as_errors] + Chef::Config[:treat_deprecation_warnings_as_errors] = false + begin + block.call + ensure + Chef::Config[:treat_deprecation_warnings_as_errors] = old_treat_deprecation_warnings_as_errors + end +end + action :pass_buck do lwrp_foo :prepared_thumbs do action :prepare_thumbs - provider :lwrp_thumb_twiddler + # We know there will be a deprecation error here; head it off + without_deprecation_warnings do + provider :lwrp_thumb_twiddler + end end lwrp_foo :twiddled_thumbs do action :twiddle_thumbs - provider :lwrp_thumb_twiddler + # We know there will be a deprecation error here; head it off + without_deprecation_warnings do + provider :lwrp_thumb_twiddler + end end end diff --git a/spec/data/lwrp/providers/buck_passer_2.rb b/spec/data/lwrp/providers/buck_passer_2.rb index d34da3c378..c3bab7266f 100644 --- a/spec/data/lwrp/providers/buck_passer_2.rb +++ b/spec/data/lwrp/providers/buck_passer_2.rb @@ -1,10 +1,26 @@ +def without_deprecation_warnings(&block) + old_treat_deprecation_warnings_as_errors = Chef::Config[:treat_deprecation_warnings_as_errors] + Chef::Config[:treat_deprecation_warnings_as_errors] = false + begin + block.call + ensure + Chef::Config[:treat_deprecation_warnings_as_errors] = old_treat_deprecation_warnings_as_errors + end +end + action :pass_buck do lwrp_bar :prepared_eyes do action :prepare_eyes - provider :lwrp_paint_drying_watcher + # We know there will be a deprecation error here; head it off + without_deprecation_warnings do + provider :lwrp_paint_drying_watcher + end end lwrp_bar :dried_paint_watched do action :watch_paint_dry - provider :lwrp_paint_drying_watcher + # We know there will be a deprecation error here; head it off + without_deprecation_warnings do + provider :lwrp_paint_drying_watcher + end end end diff --git a/spec/data/lwrp/providers/embedded_resource_accesses_providers_scope.rb b/spec/data/lwrp/providers/embedded_resource_accesses_providers_scope.rb index f5841fb01c..77c1111ff5 100644 --- a/spec/data/lwrp/providers/embedded_resource_accesses_providers_scope.rb +++ b/spec/data/lwrp/providers/embedded_resource_accesses_providers_scope.rb @@ -3,11 +3,23 @@ # are passed properly (as demonstrated by the call to generate_new_name). attr_reader :enclosed_resource +def without_deprecation_warnings(&block) + old_treat_deprecation_warnings_as_errors = Chef::Config[:treat_deprecation_warnings_as_errors] + Chef::Config[:treat_deprecation_warnings_as_errors] = false + begin + block.call + ensure + Chef::Config[:treat_deprecation_warnings_as_errors] = old_treat_deprecation_warnings_as_errors + end +end + action :twiddle_thumbs do @enclosed_resource = lwrp_foo :foo do monkey generate_new_name(new_resource.monkey){ 'the monkey' } - action :twiddle_thumbs - provider :lwrp_monkey_name_printer + # We know there will be a deprecation error here; head it off + without_deprecation_warnings do + provider :lwrp_monkey_name_printer + end end end diff --git a/spec/data/lwrp/resources/bar.rb b/spec/data/lwrp/resources/bar.rb index b6359648db..2ff35efd08 100644 --- a/spec/data/lwrp/resources/bar.rb +++ b/spec/data/lwrp/resources/bar.rb @@ -1,2 +1,2 @@ -provides "lwrp_bar" # This makes sure that we cover the case of lwrps using provides +provides :lwrp_bar # This makes sure that we cover the case of lwrps using provides actions :pass_buck, :prepare_eyes, :watch_paint_dry diff --git a/spec/data/lwrp_override/resources/foo.rb b/spec/data/lwrp_override/resources/foo.rb index 14decb9634..2fc13d32fd 100644 --- a/spec/data/lwrp_override/resources/foo.rb +++ b/spec/data/lwrp_override/resources/foo.rb @@ -3,3 +3,8 @@ actions :never_execute attribute :ever, :kind_of => String + +class ::Chef + def method_created_by_override_lwrp_foo + end +end diff --git a/spec/data/big_json.json b/spec/data/nested.json index 96c2818894..775bb21981 100644 --- a/spec/data/big_json.json +++ b/spec/data/nested.json @@ -1,2 +1,2 @@ -{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":"test" -}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}
\ No newline at end of file +{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":"test" +}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}} diff --git a/spec/data/run_context/cookbooks/include/recipes/default.rb b/spec/data/run_context/cookbooks/include/recipes/default.rb new file mode 100644 index 0000000000..8d22994252 --- /dev/null +++ b/spec/data/run_context/cookbooks/include/recipes/default.rb @@ -0,0 +1,24 @@ +module ::RanResources + def self.resources + @resources ||= [] + end +end +class RunContextCustomResource < Chef::Resource + action :create do + ruby_block '4' do + block { RanResources.resources << 4 } + end + recipe_eval do + ruby_block '1' do + block { RanResources.resources << 1 } + end + include_recipe 'include::includee' + ruby_block '3' do + block { RanResources.resources << 3 } + end + end + ruby_block '5' do + block { RanResources.resources << 5 } + end + end +end diff --git a/spec/data/run_context/cookbooks/include/recipes/includee.rb b/spec/data/run_context/cookbooks/include/recipes/includee.rb new file mode 100644 index 0000000000..87bb7f114e --- /dev/null +++ b/spec/data/run_context/cookbooks/include/recipes/includee.rb @@ -0,0 +1,3 @@ +ruby_block '2' do + block { RanResources.resources << 2 } +end diff --git a/spec/data/trusted_certs/opscode.pem b/spec/data/trusted_certs/opscode.pem index 37a3dd1ef2..e421a4e6e9 100644 --- a/spec/data/trusted_certs/opscode.pem +++ b/spec/data/trusted_certs/opscode.pem @@ -1,60 +1,57 @@ -----BEGIN CERTIFICATE----- -MIIFrDCCBJSgAwIBAgIQB1O/fCb6cEytJ4BP3HTbCTANBgkqhkiG9w0BAQUFADBI -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMSIwIAYDVQQDExlE -aWdpQ2VydCBTZWN1cmUgU2VydmVyIENBMB4XDTE0MDYxMDAwMDAwMFoXDTE1MDcw -MTEyMDAwMFowaTELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAO -BgNVBAcTB1NlYXR0bGUxGzAZBgNVBAoTEkNoZWYgU29mdHdhcmUsIEluYzEWMBQG -A1UEAwwNKi5vcHNjb2RlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBAMm+rf2RcPGBlZoM+hI4BxlaHbdRg1GZJ/T46UWFOBnZFVP++TX/pyjDsvns -xymcQywtoN/26+UIys6oWX1um9ikEokvf67LdsUeemQGFHFky8X1Ka2hVtKnxBhi -XZfvyHDR4IyFWU9AwmhnqySzxqCtynUu8Gktx7JVfqbRFMZ186pDcSw8LoaqjTVG -SzO7eNH2sM3doMueAHj7ITc2wUzmfa0Pdh+K8UoCn/HopU5LzycziJVPYvUkLT2m -YCV7VWRc+kObZseHhZAbyaDk3RgPQ/eRMhytAgbruBHWDqNesNw+ZA70w856Oj2Y -geO7JF+5V6WvkywrF8vydaoM2l8CAwEAAaOCAm8wggJrMB8GA1UdIwQYMBaAFJBx -2zfrc8jv3NUeErY0uitaoKaSMB0GA1UdDgQWBBQK5zjZwbcmcMNLnI2h1ioAldEV -ujCBygYDVR0RBIHCMIG/gg0qLm9wc2NvZGUuY29tghBjb3JwLm9wc2NvZGUuY29t -ghIqLmNvcnAub3BzY29kZS5jb22CDyoubGVhcm5jaGVmLmNvbYISKi5jb3JwLmdl -dGNoZWYuY29tgg0qLmdldGNoZWYuY29tggwqLm9wc2NvZGUudXOCC2dldGNoZWYu -Y29tggtvcHNjb2RlLmNvbYIRYXBpLmJlcmtzaGVsZi5jb22CDWxlYXJuY2hlZi5j -b22CCm9wc2NvZGUudXMwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUF -BwMBBggrBgEFBQcDAjBhBgNVHR8EWjBYMCqgKKAmhiRodHRwOi8vY3JsMy5kaWdp -Y2VydC5jb20vc3NjYS1nNi5jcmwwKqAooCaGJGh0dHA6Ly9jcmw0LmRpZ2ljZXJ0 -LmNvbS9zc2NhLWc2LmNybDBCBgNVHSAEOzA5MDcGCWCGSAGG/WwBATAqMCgGCCsG -AQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5jb20vQ1BTMHgGCCsGAQUFBwEB -BGwwajAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEIGCCsG -AQUFBzAChjZodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRTZWN1 -cmVTZXJ2ZXJDQS5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQUFAAOCAQEA -kgBpJ2t+St7SmWfeNU9EWAhy0NuUnRIi1jnqXdapfPmS6V/M0i2wP/p+crMty78e -+3ieuF5s0GJBLs85Hikcl3SlrrbIBJxozov1TY6zeOi6+TCsdXer6t6iQKz36zno -+k+T6lnMCyo9+pk1PhcAWyfo1Fz4xVOBVec/71VovFkkGD2//KB+sbDs+yh21N9M -ReO7duj16rQSctfO9R2h65djBNlgz6hXY2nlw8/x3uFfZobXOxDrTcH6Z8HIslkE -MiTXGix6zdqJaFRCWi+prnAztWs+jEy+v95VSEHPj3xpwZ9WjsxQN0kFA2EX61v/ -kGunmyhehGjblQRt7bpyiA== ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIEjzCCA3egAwIBAgIQBp4dt3/PHfupevXlyaJANzANBgkqhkiG9w0BAQUFADBh +MIIElDCCA3ygAwIBAgIQAf2j627KdciIQ4tyS8+8kTANBgkqhkiG9w0BAQsFADBh MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD -QTAeFw0xMzAzMDgxMjAwMDBaFw0yMzAzMDgxMjAwMDBaMEgxCzAJBgNVBAYTAlVT -MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxIjAgBgNVBAMTGURpZ2lDZXJ0IFNlY3Vy -ZSBTZXJ2ZXIgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC7V+Qh -qdWbYDd+jqFhf4HiGsJ1ZNmRUAvkNkQkbjDSm3on+sJqrmpwCTi5IArIZRBKiKwx -8tyS8mOhXYBjWYCSIxzm73ZKUDXJ2HE4ue3w5kKu0zgmeTD5IpTG26Y/QXiQ2N5c -fml9+JAVOtChoL76srIZodgr0c6/a91Jq6OS/rWryME+7gEA2KlEuEJziMNh9atK -gygK0tRJ+mqxzd9XLJTl4sqDX7e6YlwvaKXwwLn9K9HpH9gaYhW9/z2m98vv5ttl -LyU47PvmIGZYljQZ0hXOIdMkzNkUb9j+Vcfnb7YPGoxJvinyulqagSY3JG/XSBJs -Lln1nBi72fZo4t9FAgMBAAGjggFaMIIBVjASBgNVHRMBAf8ECDAGAQH/AgEAMA4G -A1UdDwEB/wQEAwIBhjA0BggrBgEFBQcBAQQoMCYwJAYIKwYBBQUHMAGGGGh0dHA6 -Ly9vY3NwLmRpZ2ljZXJ0LmNvbTB7BgNVHR8EdDByMDegNaAzhjFodHRwOi8vY3Js -My5kaWdpY2VydC5jb20vRGlnaUNlcnRHbG9iYWxSb290Q0EuY3JsMDegNaAzhjFo -dHRwOi8vY3JsNC5kaWdpY2VydC5jb20vRGlnaUNlcnRHbG9iYWxSb290Q0EuY3Js -MD0GA1UdIAQ2MDQwMgYEVR0gADAqMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5k -aWdpY2VydC5jb20vQ1BTMB0GA1UdDgQWBBSQcds363PI79zVHhK2NLorWqCmkjAf -BgNVHSMEGDAWgBQD3lA1VtFMu2bwo+IbG8OXsj3RVTANBgkqhkiG9w0BAQUFAAOC -AQEAMM7RlVEArgYLoQ4CwBestn+PIPZAdXQczHixpE/q9NDEnaLegQcmH0CIUfAf -z7dMQJnQ9DxxmHOIlywZ126Ej6QfnFog41FcsMWemWpPyGn3EP9OrRnZyVizM64M -2ZYpnnGycGOjtpkWQh1l8/egHn3F1GUUsmKE1GxcCAzYbJMrtHZZitF//wPYwl24 -LyLWOPD2nGt9RuuZdPfrSg6ppgTre87wXGuYMVqYQOtpxAX0IKjKCDplbDgV9Vws -slXkLGtB8L5cRspKKaBIXiDSRf8F3jSvcEuBOeLKB1d8tjHcISnivpcOd5AUUUDh -v+PMGxmcJcqnBrJT3yOyzxIZow== +QTAeFw0xMzAzMDgxMjAwMDBaFw0yMzAzMDgxMjAwMDBaME0xCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxJzAlBgNVBAMTHkRpZ2lDZXJ0IFNIQTIg +U2VjdXJlIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +ANyuWJBNwcQwFZA1W248ghX1LFy949v/cUP6ZCWA1O4Yok3wZtAKc24RmDYXZK83 +nf36QYSvx6+M/hpzTc8zl5CilodTgyu5pnVILR1WN3vaMTIa16yrBvSqXUu3R0bd +KpPDkC55gIDvEwRqFDu1m5K+wgdlTvza/P96rtxcflUxDOg5B6TXvi/TC2rSsd9f +/ld0Uzs1gN2ujkSYs58O09rg1/RrKatEp0tYhG2SS4HD2nOLEpdIkARFdRrdNzGX +kujNVA075ME/OV4uuPNcfhCOhkEAjUVmR7ChZc6gqikJTvOX6+guqw9ypzAO+sf0 +/RR3w6RbKFfCs/mC/bdFWJsCAwEAAaOCAVowggFWMBIGA1UdEwEB/wQIMAYBAf8C +AQAwDgYDVR0PAQH/BAQDAgGGMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEFBQcwAYYY +aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMHsGA1UdHwR0MHIwN6A1oDOGMWh0dHA6 +Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RDQS5jcmwwN6A1 +oDOGMWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RD +QS5jcmwwPQYDVR0gBDYwNDAyBgRVHSAAMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8v +d3d3LmRpZ2ljZXJ0LmNvbS9DUFMwHQYDVR0OBBYEFA+AYRyCMWHVLyjnjUY4tCzh +xtniMB8GA1UdIwQYMBaAFAPeUDVW0Uy7ZvCj4hsbw5eyPdFVMA0GCSqGSIb3DQEB +CwUAA4IBAQAjPt9L0jFCpbZ+QlwaRMxp0Wi0XUvgBCFsS+JtzLHgl4+mUwnNqipl +5TlPHoOlblyYoiQm5vuh7ZPHLgLGTUq/sELfeNqzqPlt/yGFUzZgTHbO7Djc1lGA +8MXW5dRNJ2Srm8c+cftIl7gzbckTB+6WohsYFfZcTEDts8Ls/3HB40f/1LkAtDdC +2iDJ6m6K7hQGrn2iWZiIqBtvLfTyyRRfJs8sjX7tN8Cp1Tm5gr8ZDOo0rwAhaPit +c+LJMto4JQtV05od8GiG7S5BNO98pVAdvzr508EIDObtHopYJeS4d60tbvVS3bR0 +j6tJLp07kzQoH3jOlOrHvdPJbRzeXDLz +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFDTCCA/WgAwIBAgIQBZ8R1sZP2Lbc8x554UUQ2DANBgkqhkiG9w0BAQsFADBN +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMScwJQYDVQQDEx5E +aWdpQ2VydCBTSEEyIFNlY3VyZSBTZXJ2ZXIgQ0EwHhcNMTQxMTEwMDAwMDAwWhcN +MTcxMTE0MTIwMDAwWjBlMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv +bjEQMA4GA1UEBxMHU2VhdHRsZTEbMBkGA1UEChMSQ2hlZiBTb2Z0d2FyZSwgSW5j +MRIwEAYDVQQDDAkqLmNoZWYuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQC3xCIczkV10O5jTDpbd4YlPLC6kfnVoOkno2N/OOlcLQu3ulj/Lj1j4r6e +2XthJLcFgTO+y+1/IKnnpLKDfkx1YngWEBXEBP+MrrpDUKKs053s45/bI9QBPISA +tXgnYxMH9Glo6FWWd13TUq++OKGw1p1wazH64XK4MAf5y/lkmWXIWumNuO35ZqtB +ME3wJISwVHzHB2CQjlDklt+Mb0APEiIFIZflgu9JNBYzLdvUtxiz15FUZQI7SsYL +TfXOD1KBNMWqN8snG2e5gRAzB2D161DFvAZt8OiYUe+3QurNlTYVzeHv1ok6UqgM +ZcLzg8m801rRip0D7FCGvMCU/ktdAgMBAAGjggHPMIIByzAfBgNVHSMEGDAWgBQP +gGEcgjFh1S8o541GOLQs4cbZ4jAdBgNVHQ4EFgQUwldjw4Pb4HV+wxGZ7MSSRh+d +pm4wHQYDVR0RBBYwFIIJKi5jaGVmLmlvggdjaGVmLmlvMA4GA1UdDwEB/wQEAwIF +oDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwawYDVR0fBGQwYjAvoC2g +K4YpaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL3NzY2Etc2hhMi1nMy5jcmwwL6At +oCuGKWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9zc2NhLXNoYTItZzMuY3JsMEIG +A1UdIAQ7MDkwNwYJYIZIAYb9bAEBMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3 +LmRpZ2ljZXJ0LmNvbS9DUFMwfAYIKwYBBQUHAQEEcDBuMCQGCCsGAQUFBzABhhho +dHRwOi8vb2NzcC5kaWdpY2VydC5jb20wRgYIKwYBBQUHMAKGOmh0dHA6Ly9jYWNl +cnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFNIQTJTZWN1cmVTZXJ2ZXJDQS5jcnQw +DAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAvcTWenNuvvrhX2omm8LQ +zWOuu8jqpoflACwD4lOSZ4TgOe4pQGCjXq8aRBD5k+goqQrPVf9lHnelUHFQac0Q +5WT4YUmisUbF0S4uY5OGQymM52MvUWG4ODL4gaWhFvN+HAXrDPP/9iitsjV0QOnl +CDq7Q4/XYRYW3opu5nLLbfW6v4QvF5yzZagEACGs7Vt32p6l391UcU8f6wiB3uMD +eioCvjpv/+2YOUNlDPCM3uBubjUhHOwO817wBxXkzdk1OSRe4jzcw/uX6wL7birt +fbaSkpilvVX529pSzB2Lvi9xWOoGMM578dpQ0h3PwhmmvKhhCWP+pI05k3oSkYCP +ng== -----END CERTIFICATE----- diff --git a/spec/functional/audit/runner_spec.rb b/spec/functional/audit/runner_spec.rb index 494942889a..aae8fcf582 100644 --- a/spec/functional/audit/runner_spec.rb +++ b/spec/functional/audit/runner_spec.rb @@ -46,22 +46,12 @@ describe Chef::Audit::Runner do RSpec::Core::Sandbox.sandboxed { ex.run } end - before do - Chef::Config[:log_location] = stdout - end - describe "#run" do let(:audits) { {} } let(:run_context) { instance_double(Chef::RunContext, :events => events, :audits => audits) } let(:control_group_name) { "control_group_name" } - it "Correctly runs an empty controls block" do - in_sub_process do - runner.run - end - end - shared_context "passing audit" do let(:audits) do should_pass = lambda do @@ -84,50 +74,40 @@ describe Chef::Audit::Runner do end end - context "there is a single successful control" do - include_context "passing audit" - it "correctly runs" do - in_sub_process do - runner.run - - expect(stdout.string).to match(/1 example, 0 failures/) + describe "log location is stdout" do + before do + allow(Chef::Log).to receive(:info) do |msg| + stdout.puts(msg) end end - end - context "there is a single failing control" do - include_context "failing audit" - it "correctly runs" do + it "Correctly runs an empty controls block" do in_sub_process do runner.run - - expect(stdout.string).to match(/Failure\/Error: expect\(2 - 1\)\.to eq\(0\)/) - expect(stdout.string).to match(/1 example, 1 failure/) - expect(stdout.string).to match(/# control_group_name should fail/) end end - end - describe "log location is a file" do - let(:tmpfile) { Tempfile.new("audit") } - before do - Chef::Config[:log_location] = tmpfile.path - end + context "there is a single successful control" do + include_context "passing audit" + it "correctly runs" do + in_sub_process do + runner.run - after do - tmpfile.close - tmpfile.unlink + expect(stdout.string).to match(/1 example, 0 failures/) + end + end end - include_context "failing audit" - it "correctly runs" do - in_sub_process do - runner.run + context "there is a single failing control" do + include_context "failing audit" + it "correctly runs" do + in_sub_process do + runner.run - contents = tmpfile.read - expect(contents).to match(/Failure\/Error: expect\(2 - 1\)\.to eq\(0\)/) - expect(contents).to match(/1 example, 1 failure/) - expect(contents).to match(/# control_group_name should fail/) + expect(stdout.string).to match(/Failure\/Error: expect\(2 - 1\)\.to eq\(0\)/) + expect(stdout.string).to match(/1 example, 1 failure/) + expect(stdout.string).to match(/# control_group_name should fail/) + end end end end diff --git a/spec/functional/event_loggers/windows_eventlog_spec.rb b/spec/functional/event_loggers/windows_eventlog_spec.rb index 4e383dd429..0723e7b984 100644 --- a/spec/functional/event_loggers/windows_eventlog_spec.rb +++ b/spec/functional/event_loggers/windows_eventlog_spec.rb @@ -79,4 +79,18 @@ describe Chef::EventLoggers::WindowsEventLogger, :windows_only, :not_supported_o end).to be_truthy end + it 'writes run_failed event with event_id 10003 even when run_status is not set' do + logger.run_failed(mock_exception) + + expect(event_log.read(flags, offset).any? do |e| + e.source == 'Chef' && e.event_id == 10003 && + e.string_inserts[0].include?("UNKNOWN") && + e.string_inserts[1].include?("UNKNOWN") && + e.string_inserts[2].include?(mock_exception.class.name) && + e.string_inserts[3].include?(mock_exception.message) && + e.string_inserts[4].include?(mock_exception.backtrace[0]) && + e.string_inserts[4].include?(mock_exception.backtrace[1]) + end).to be_truthy + end + end diff --git a/spec/functional/file_content_management/deploy_strategies_spec.rb b/spec/functional/file_content_management/deploy_strategies_spec.rb index 03a6c504c1..8a995d0e41 100644 --- a/spec/functional/file_content_management/deploy_strategies_spec.rb +++ b/spec/functional/file_content_management/deploy_strategies_spec.rb @@ -43,7 +43,7 @@ shared_examples_for "a content deploy strategy" do ## # UNIX Context - let(:default_mode) { normalize_mode(0100666 - File.umask) } + let(:default_mode) { normalize_mode(0666 & ~File.umask) } it "touches the file to create it (UNIX)", :unix_only do content_deployer.create(target_file_path) diff --git a/spec/functional/knife/ssh_spec.rb b/spec/functional/knife/ssh_spec.rb index 5b8ad6f368..6608d05771 100644 --- a/spec/functional/knife/ssh_spec.rb +++ b/spec/functional/knife/ssh_spec.rb @@ -165,7 +165,7 @@ describe Chef::Knife::Ssh do it "uses the ssh_attribute" do @knife.run - expect(@knife.config[:attribute]).to eq("ec2.public_hostname") + expect(@knife.get_ssh_attribute(Chef::Node.new)).to eq("ec2.public_hostname") end end @@ -177,7 +177,7 @@ describe Chef::Knife::Ssh do it "uses the default" do @knife.run - expect(@knife.config[:attribute]).to eq("fqdn") + expect(@knife.get_ssh_attribute(Chef::Node.new)).to eq("fqdn") end end diff --git a/spec/functional/mixin/powershell_out_spec.rb b/spec/functional/mixin/powershell_out_spec.rb new file mode 100644 index 0000000000..9cc8aeed7e --- /dev/null +++ b/spec/functional/mixin/powershell_out_spec.rb @@ -0,0 +1,43 @@ +# +# 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' +require 'chef/mixin/powershell_out' + +describe Chef::Mixin::PowershellOut, windows_only: true do + include Chef::Mixin::PowershellOut + + describe "#powershell_out" do + it "runs a powershell command and collects stdout" do + expect(powershell_out("get-process").run_command.stdout).to match /Handles\s+NPM\(K\)\s+PM\(K\)\s+WS\(K\)\s+VM\(M\)\s+CPU\(s\)\s+Id\s+ProcessName/ + end + + it "does not raise exceptions when the command is invalid" do + powershell_out("this-is-not-a-valid-command").run_command + end + end + + describe "#powershell_out!" do + it "runs a powershell command and collects stdout" do + expect(powershell_out!("get-process").run_command.stdout).to match /Handles\s+NPM\(K\)\s+PM\(K\)\s+WS\(K\)\s+VM\(M\)\s+CPU\(s\)\s+Id\s+ProcessName/ + end + + it "raises exceptions when the command is invalid" do + expect { powershell_out!("this-is-not-a-valid-command").run_command }.to raise_exception(Mixlib::ShellOut::ShellCommandFailed) + end + end +end diff --git a/spec/functional/provider/whyrun_safe_ruby_block_spec.rb b/spec/functional/provider/whyrun_safe_ruby_block_spec.rb index b3c2333e9a..2b582feb05 100644 --- a/spec/functional/provider/whyrun_safe_ruby_block_spec.rb +++ b/spec/functional/provider/whyrun_safe_ruby_block_spec.rb @@ -43,7 +43,7 @@ describe Chef::Resource::WhyrunSafeRubyBlock do end it "updates the evil laugh, even in why-run mode" do - new_resource.run_action(new_resource.action) + Array(new_resource.action).each {|action| new_resource.run_action(action) } expect($evil_global_evil_laugh).to eq(:mwahahaha) expect(new_resource).to be_updated end diff --git a/spec/functional/rebooter_spec.rb b/spec/functional/rebooter_spec.rb index 763021607b..a0e2665de5 100644 --- a/spec/functional/rebooter_spec.rb +++ b/spec/functional/rebooter_spec.rb @@ -43,7 +43,7 @@ describe Chef::Platform::Rebooter do let(:expected) do { - :windows => 'shutdown /r /t 5 /c "rebooter spec test"', + :windows => 'shutdown /r /t 300 /c "rebooter spec test"', :linux => 'shutdown -r +5 "rebooter spec test"' } end @@ -70,7 +70,7 @@ describe Chef::Platform::Rebooter do shared_context 'test a reboot method' do def test_rebooter_method(method_sym, is_windows, expected_reboot_str) - allow(Chef::Platform).to receive(:windows?).and_return(is_windows) + allow(ChefConfig).to receive(:windows?).and_return(is_windows) expect(rebooter).to receive(:shell_out!).once.with(expected_reboot_str) expect(rebooter).to receive(method_sym).once.and_call_original rebooter.send(method_sym, run_context.node) diff --git a/spec/functional/resource/aixinit_service_spec.rb b/spec/functional/resource/aixinit_service_spec.rb index 19b65ca2a0..3d9216158e 100755 --- a/spec/functional/resource/aixinit_service_spec.rb +++ b/spec/functional/resource/aixinit_service_spec.rb @@ -208,4 +208,4 @@ describe Chef::Resource::Service, :requires_root, :aix_only do end end end -end
\ No newline at end of file +end diff --git a/spec/functional/resource/cookbook_file_spec.rb b/spec/functional/resource/cookbook_file_spec.rb index 7797ed0041..6d4c5b4a8f 100644 --- a/spec/functional/resource/cookbook_file_spec.rb +++ b/spec/functional/resource/cookbook_file_spec.rb @@ -32,7 +32,7 @@ describe Chef::Resource::CookbookFile do content end - let(:default_mode) { ((0100666 - File.umask) & 07777).to_s(8) } + let(:default_mode) { (0666 & ~File.umask).to_s(8) } it_behaves_like "a securable resource with reporting" diff --git a/spec/functional/resource/directory_spec.rb b/spec/functional/resource/directory_spec.rb index 2c4025f83e..88a810964f 100644 --- a/spec/functional/resource/directory_spec.rb +++ b/spec/functional/resource/directory_spec.rb @@ -23,7 +23,7 @@ describe Chef::Resource::Directory do let(:directory_base) { "directory_spec" } - let(:default_mode) { ((0100777 - File.umask) & 07777).to_s(8) } + let(:default_mode) { (0777 & ~File.umask).to_s(8) } def create_resource events = Chef::EventDispatch::Dispatcher.new diff --git a/spec/functional/resource/dsc_resource_spec.rb b/spec/functional/resource/dsc_resource_spec.rb new file mode 100644 index 0000000000..6f453eeb9f --- /dev/null +++ b/spec/functional/resource/dsc_resource_spec.rb @@ -0,0 +1,93 @@ +# +# Author:: Jay Mundrawala (<jdm@chef.io>) +# 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' + +describe Chef::Resource::DscResource, :windows_powershell_dsc_only do + before(:all) do + @ohai = Ohai::System.new + @ohai.all_plugins(['platform', 'os', 'languages/powershell']) + end + + let(:event_dispatch) { Chef::EventDispatch::Dispatcher.new } + + let(:node) { + Chef::Node.new.tap do |n| + n.consume_external_attrs(@ohai.data, {}) + end + } + + let(:run_context) { Chef::RunContext.new(node, {}, event_dispatch) } + + let(:new_resource) { + Chef::Resource::DscResource.new("dsc_resource_test", run_context) + } + + context 'when Powershell does not support Invoke-DscResource' + context 'when Powershell supports Invoke-DscResource' do + before do + if !Chef::Platform.supports_dsc_invoke_resource?(node) + skip 'Requires Powershell >= 5.0.10018.0' + end + end + context 'with an invalid dsc resource' do + it 'raises an exception if the resource is not found' do + new_resource.resource 'thisdoesnotexist' + expect { new_resource.run_action(:run) }.to raise_error( + Chef::Exceptions::ResourceNotFound) + end + end + + context 'with a valid dsc resource' do + let(:tmp_file_name) { Dir::Tmpname.create('tmpfile') {} } + let(:test_text) { "'\"!@#$%^&*)(}{][\u2713~n"} + + before do + new_resource.resource :File + new_resource.property :Contents, test_text + new_resource.property :DestinationPath, tmp_file_name + end + + after do + File.delete(tmp_file_name) if File.exists? tmp_file_name + end + + it 'converges the resource if it is not converged' do + new_resource.run_action(:run) + contents = File.open(tmp_file_name, 'rb:bom|UTF-16LE') do |f| + f.read.encode('UTF-8') + end + expect(contents).to eq(test_text) + expect(new_resource).to be_updated + end + + it 'does not converge the resource if it is already converged' do + new_resource.run_action(:run) + expect(new_resource).to be_updated + reresource = + Chef::Resource::DscResource.new("dsc_resource_retest", run_context) + reresource.resource :File + reresource.property :Contents, test_text + reresource.property :DestinationPath, tmp_file_name + reresource.run_action(:run) + expect(reresource).not_to be_updated + end + end + + end +end diff --git a/spec/functional/resource/dsc_script_spec.rb b/spec/functional/resource/dsc_script_spec.rb index a736949c6b..dc7704481f 100644 --- a/spec/functional/resource/dsc_script_spec.rb +++ b/spec/functional/resource/dsc_script_spec.rb @@ -19,6 +19,7 @@ require 'spec_helper' require 'chef/mixin/shell_out' require 'chef/mixin/windows_architecture_helper' +require 'support/shared/integration/integration_helper' describe Chef::Resource::DscScript, :windows_powershell_dsc_only do include Chef::Mixin::WindowsArchitectureHelper @@ -67,8 +68,7 @@ describe Chef::Resource::DscScript, :windows_powershell_dsc_only do node = Chef::Node.new node.automatic['platform'] = 'windows' node.automatic['platform_version'] = '6.1' - node.automatic['kernel'][:machine] = - is_i386_process_on_x86_64_windows? ? :x86_64 : :i386 + node.automatic['kernel'][:machine] = :x86_64 # Only 64-bit architecture is supported node.automatic[:languages][:powershell][:version] = '4.0' empty_events = Chef::EventDispatch::Dispatcher.new Chef::RunContext.new(node, {}, empty_events) @@ -379,4 +379,93 @@ EOH it_behaves_like 'a dsc_script with configuration data that takes parameters' it_behaves_like 'a dsc_script without configuration data that takes parameters' end + + context 'when using ps_credential' do + include IntegrationSupport + + before(:each) do + delete_user(dsc_user) + ohai_reader = Ohai::System.new + ohai_reader.all_plugins(["platform", "os", "languages/powershell"]) + dsc_test_run_context.node.consume_external_attrs(ohai_reader.data,{}) + end + + let(:configuration_data_path) { 'C:\\configurationdata.psd1' } + + let(:self_signed_cert_path) do + File.join(CHEF_SPEC_DATA, 'dsc_lcm.pfx') + end + + let(:dsc_configuration_script) do + <<-MYCODE +cd c:\\ +configuration LCM +{ + param ($thumbprint) + localconfigurationmanager + { + RebootNodeIfNeeded = $false + ConfigurationMode = 'ApplyOnly' + CertificateID = $thumbprint + } +} +$cert = ls Cert:\\LocalMachine\\My\\ | + Where-Object {$_.Subject -match "ChefTest"} | + Select -first 1 + +if($cert -eq $null) { + $pfxpath = '#{self_signed_cert_path}' + $password = '' + $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($pfxpath, $password, ([System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeyset)) + $store = New-Object System.Security.Cryptography.X509Certificates.X509Store "My", ([System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine) + $store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite) + $store.Add($cert) + $store.Close() +} + +lcm -thumbprint $cert.thumbprint +set-dsclocalconfigurationmanager -path ./LCM +$ConfigurationData = @" +@{ +AllNodes = @( + @{ + NodeName = "localhost"; + CertificateID = '$($cert.thumbprint)'; + }; +); +} +"@ +$ConfigurationData | out-file '#{configuration_data_path}' -force + MYCODE + end + + let(:powershell_script_resource) do + Chef::Resource::PowershellScript.new('configure-lcm', dsc_test_run_context).tap do |r| + r.code(dsc_configuration_script) + r.architecture(:x86_64) + end + end + + let(:dsc_script_resource) do + dsc_test_resource_base.tap do |r| + r.code <<-EOF +User dsctestusercreate +{ + UserName = '#{dsc_user}' + Password = #{r.ps_credential('jf9a8m49jrajf4#')} + Ensure = "Present" +} +EOF + r.configuration_data_script(configuration_data_path) + end + end + + it 'allows the use of ps_credential' do + expect(user_exists?(dsc_user)).to eq(false) + powershell_script_resource.run_action(:run) + expect(File).to exist(configuration_data_path) + dsc_script_resource.run_action(:run) + expect(user_exists?(dsc_user)).to eq(true) + end + end end diff --git a/spec/functional/resource/env_spec.rb b/spec/functional/resource/env_spec.rb index 16caec14bf..b9dcd7b33a 100755 --- a/spec/functional/resource/env_spec.rb +++ b/spec/functional/resource/env_spec.rb @@ -29,14 +29,15 @@ describe Chef::Resource::Env, :windows_only do let(:env_value_expandable) { '%SystemRoot%' } let(:test_run_context) { node = Chef::Node.new + node.default['os'] = 'windows' node.default['platform'] = 'windows' node.default['platform_version'] = '6.1' empty_events = Chef::EventDispatch::Dispatcher.new Chef::RunContext.new(node, {}, empty_events) } - let(:test_resource) { - Chef::Resource::Env.new('unknown', test_run_context) - } + let(:test_resource) { + Chef::Resource::Env.new('unknown', test_run_context) + } before(:each) do resource_lower = Chef::Resource::Env.new(chef_env_test_lower_case, test_run_context) diff --git a/spec/functional/resource/execute_spec.rb b/spec/functional/resource/execute_spec.rb index aaa1c772b7..692ccfb796 100644 --- a/spec/functional/resource/execute_spec.rb +++ b/spec/functional/resource/execute_spec.rb @@ -62,7 +62,7 @@ describe Chef::Resource::Execute do end describe "when parent resource sets :cwd" do - let(:guard) { %{ruby -e 'exit 1 unless File.exists?("./big_json_plus_one.json")'} } + let(:guard) { %{ruby -e 'exit 1 unless File.exists?("./nested.json")'} } it "guard inherits :cwd from resource and runs" do resource.cwd CHEF_SPEC_DATA @@ -137,9 +137,16 @@ describe Chef::Resource::Execute do end end + # Ensure that CommandTimeout is raised, and is caused by resource.timeout really expiring. + # https://github.com/chef/chef/issues/2985 + # + # resource.timeout should be short, this is what we're testing + # resource.command ruby sleep timer should be longer than resource.timeout to give us something to timeout + # Timeout::timeout should be longer than resource.timeout, but less than the resource.command ruby sleep timer, + # so we fail if we finish on resource.command instead of resource.timeout, but raise CommandTimeout anyway (#2175). it "times out when a timeout is set on the resource" do - Timeout::timeout(5) do - resource.command %{ruby -e 'sleep 600'} + Timeout::timeout(30) do + resource.command %{ruby -e 'sleep 300'} resource.timeout 0.1 expect { resource.run_action(:run) }.to raise_error(Mixlib::ShellOut::CommandTimeout) end diff --git a/spec/functional/resource/file_spec.rb b/spec/functional/resource/file_spec.rb index cf70c349fb..9e30e62111 100644 --- a/spec/functional/resource/file_spec.rb +++ b/spec/functional/resource/file_spec.rb @@ -64,7 +64,7 @@ describe Chef::Resource::File do provider.current_resource end - let(:default_mode) { ((0100666 - File.umask) & 07777).to_s(8) } + let(:default_mode) { (0666 & ~File.umask).to_s(8) } it_behaves_like "a file resource" @@ -86,6 +86,31 @@ describe Chef::Resource::File do end end + + describe "when using backup" do + before do + Chef::Config[:file_backup_path] = CHEF_SPEC_BACKUP_PATH + resource_without_content.backup(1) + resource_without_content.run_action(:create) + end + + let(:backup_glob) { File.join(CHEF_SPEC_BACKUP_PATH, test_file_dir.sub(/^([A-Za-z]:)/, ""), "#{file_base}*") } + + let(:path) do + # Use native system path + ChefConfig::PathHelper.canonical_path(File.join(test_file_dir, make_tmpname(file_base)), false) + end + + it "only stores the number of requested backups" do + resource_without_content.content('foo') + resource_without_content.run_action(:create) + resource_without_content.content('bar') + resource_without_content.run_action(:create) + expect(Dir.glob(backup_glob).length).to eq(1) + end + + end + # github issue 1842. describe "when running action :create on a relative path" do before do diff --git a/spec/functional/resource/group_spec.rb b/spec/functional/resource/group_spec.rb index 6676aa32e9..0862b8e15f 100644 --- a/spec/functional/resource/group_spec.rb +++ b/spec/functional/resource/group_spec.rb @@ -95,7 +95,7 @@ describe Chef::Resource::Group, :requires_root_or_running_windows, :not_supporte def create_user(username) user(username).run_action(:create) if ! windows_domain_user?(username) - # TODO: User shouldn't exist + # TODO: User should exist end def remove_user(username) @@ -135,45 +135,76 @@ describe Chef::Resource::Group, :requires_root_or_running_windows, :not_supporte group_should_not_exist(group_name) end - describe "when append is not set" do - let(:included_members) { [spec_members[1]] } + # dscl doesn't perform any error checking and will let you add users that don't exist. + describe "when no users exist", :not_supported_on_mac_osx do + describe "when append is not set" do + # excluded_members can only be used when append is set. It is ignored otherwise. + let(:excluded_members) { [] } - before do - create_user(spec_members[1]) - create_user(spec_members[0]) - add_members_to_group([spec_members[0]]) - end - - after do - remove_user(spec_members[1]) - remove_user(spec_members[0]) + it "should raise an error" do + expect { group_resource.run_action(tested_action) }.to raise_error() + end end - it "should remove the existing users and add the new users to the group" do - group_resource.run_action(tested_action) + describe "when append is set" do + before do + group_resource.append(true) + end - expect(user_exist_in_group?(spec_members[1])).to eq(true) - expect(user_exist_in_group?(spec_members[0])).to eq(false) + it "should raise an error" do + expect { group_resource.run_action(tested_action) }.to raise_error() + end end end - describe "when append is set" do - before(:each) do - group_resource.append(true) + describe "when the users exist" do + before do + (spec_members).each do |member| + create_user(member) + end end - describe "when the users exist" do - before do - (included_members + excluded_members).each do |member| - create_user(member) + after do + (spec_members).each do |member| + remove_user(member) + end + end + + describe "when append is not set" do + it "should set the group to to contain given members" do + group_resource.run_action(tested_action) + + included_members.each do |member| + expect(user_exist_in_group?(member)).to eq(true) + end + (spec_members - included_members).each do |member| + expect(user_exist_in_group?(member)).to eq(false) end end - after do - (included_members + excluded_members).each do |member| - remove_user(member) + describe "when group already contains some users" do + before do + add_members_to_group([included_members[0]]) + add_members_to_group(spec_members - included_members) + end + + it "should remove all existing users and only add the new users to the group" do + group_resource.run_action(tested_action) + + included_members.each do |member| + expect(user_exist_in_group?(member)).to eq(true) + end + (spec_members - included_members).each do |member| + expect(user_exist_in_group?(member)).to eq(false) + end end end + end + + describe "when append is set" do + before(:each) do + group_resource.append(true) + end it "should add included members to the group" do group_resource.run_action(tested_action) @@ -186,9 +217,9 @@ describe Chef::Resource::Group, :requires_root_or_running_windows, :not_supporte end end - describe "when group contains some users" do + describe "when group already contains some users" do before(:each) do - add_members_to_group([ spec_members[0], spec_members[2] ]) + add_members_to_group([included_members[0], excluded_members[0]]) end it "should add the included users and remove excluded users" do @@ -203,20 +234,6 @@ describe Chef::Resource::Group, :requires_root_or_running_windows, :not_supporte end end end - - describe "when the users doesn't exist" do - describe "when append is not set" do - it "should raise an error" do - expect { @grp_resource.run_action(tested_action) }.to raise_error - end - end - - describe "when append is set" do - it "should raise an error" do - expect { @grp_resource.run_action(tested_action) }.to raise_error - end - end - end end end @@ -231,6 +248,12 @@ describe Chef::Resource::Group, :requires_root_or_running_windows, :not_supporte group_should_exist(group_name) end + after(:each) do + group_resource.run_action(:remove) + end + + # TODO: The ones below might actually return ArgumentError now - but I don't have + # a way to verify that. Change it and delete this comment if that's the case. describe "when updating membership" do it "raises an error for a non well-formed domain name" do group_resource.members [invalid_domain_user_name] @@ -256,7 +279,7 @@ describe Chef::Resource::Group, :requires_root_or_running_windows, :not_supporte end end - let(:group_name) { "t-#{SecureRandom.random_number(9999)}" } + let(:group_name) { "group#{SecureRandom.random_number(9999)}" } let(:included_members) { nil } let(:excluded_members) { nil } let(:group_resource) { @@ -300,7 +323,7 @@ theoldmanwalkingdownthestreetalwayshadagoodsmileonhisfacetheoldmanwalking\ downthestreetalwayshadagoodsmileonhisfacetheoldmanwalkingdownthestreeQQQQQQ" } it "should not create a group" do - expect { group_resource.run_action(:create) }.to raise_error + expect { group_resource.run_action(:create) }.to raise_error(ArgumentError) group_should_not_exist(group_name) end end @@ -372,6 +395,11 @@ downthestreetalwayshadagoodsmileonhisfacetheoldmanwalkingdownthestreeQQQQQQ" } let(:tested_action) { :manage } describe "when there is no group" do + before(:each) do + group_resource.run_action(:remove) + group_should_not_exist(group_name) + end + it "raises an error on modify" do expect { group_resource.run_action(:modify) }.to raise_error end diff --git a/spec/functional/resource/link_spec.rb b/spec/functional/resource/link_spec.rb index d39a0c2ef6..7e903b30b4 100644 --- a/spec/functional/resource/link_spec.rb +++ b/spec/functional/resource/link_spec.rb @@ -348,8 +348,7 @@ describe Chef::Resource::Link do end it_behaves_like 'delete errors out' end - context 'and the link already exists and is not writeable to this user', :skip => true do - end + it_behaves_like 'a securable resource without existing target' do let(:path) { target_file } def allowed_acl(sid, expected_perms) @@ -360,7 +359,7 @@ describe Chef::Resource::Link do end def parent_inheritable_acls dummy_file_path = File.join(test_file_dir, "dummy_file") - dummy_file = FileUtils.touch(dummy_file_path) + FileUtils.touch(dummy_file_path) dummy_desc = get_security_descriptor(dummy_file_path) FileUtils.rm_rf(dummy_file_path) dummy_desc @@ -416,8 +415,6 @@ describe Chef::Resource::Link do end end end - context "when the link destination is not readable to this user", :skip => true do - end context "when the link destination does not exist" do include_context 'create symbolic link succeeds' include_context 'delete is noop' @@ -518,8 +515,6 @@ describe Chef::Resource::Link do end it_behaves_like 'delete errors out' end - context "and the link already exists and is not writeable to this user", :skip => true do - end context "and specifies security attributes" do before(:each) do resource.owner(windows? ? 'Guest' : 'nobody') @@ -559,10 +554,10 @@ describe Chef::Resource::Link do end context 'and the link does not yet exist' do it 'links to the target file' do + skip('OS X/FreeBSD/AIX symlink? and readlink working on hard links to symlinks') if (os_x? or freebsd? or aix?) resource.run_action(:create) expect(File.exists?(target_file)).to be_truthy # OS X gets angry about this sort of link. Bug in OS X, IMO. - pending('OS X/FreeBSD/AIX symlink? and readlink working on hard links to symlinks') if (os_x? or freebsd? or aix?) expect(symlink?(target_file)).to be_truthy expect(readlink(target_file)).to eq(canonicalize(@other_target)) end @@ -578,7 +573,7 @@ describe Chef::Resource::Link do end context 'and the link does not yet exist' do it 'links to the target file' do - pending('OS X/FreeBSD/AIX fails to create hardlinks to broken symlinks') if (os_x? or freebsd? or aix?) + skip('OS X/FreeBSD/AIX fails to create hardlinks to broken symlinks') if (os_x? or freebsd? or aix?) resource.run_action(:create) # Windows and Unix have different definitions of exists? here, and that's OK. if windows? @@ -593,8 +588,7 @@ describe Chef::Resource::Link do end end end - context "when the link destination is not readable to this user", :skip => true do - end + context "when the link destination does not exist" do context 'and the link does not yet exist' do it 'create errors out' do diff --git a/spec/functional/resource/package_spec.rb b/spec/functional/resource/package_spec.rb index 5c17ca0107..8d37b072e8 100644 --- a/spec/functional/resource/package_spec.rb +++ b/spec/functional/resource/package_spec.rb @@ -386,5 +386,3 @@ describe Chef::Resource::Package, metadata do end end - - diff --git a/spec/functional/resource/powershell_spec.rb b/spec/functional/resource/powershell_script_spec.rb index 1b3ac844e0..be744e748b 100644 --- a/spec/functional/resource/powershell_spec.rb +++ b/spec/functional/resource/powershell_script_spec.rb @@ -56,13 +56,13 @@ describe Chef::Resource::WindowsScript::PowershellScript, :windows_only do resource.run_action(:run) end - it "returns the -27 for a powershell script that exits with -27" do + it "returns the exit status 27 for a powershell script that exits with 27" do file = Tempfile.new(['foo', '.ps1']) begin - file.write "exit -27" + file.write "exit 27" file.close resource.code(". \"#{file.path}\"") - resource.returns(-27) + resource.returns(27) resource.run_action(:run) ensure file.close @@ -70,6 +70,30 @@ describe Chef::Resource::WindowsScript::PowershellScript, :windows_only do end end + let (:negative_exit_status) { -27 } + let (:unsigned_exit_status) { (-negative_exit_status ^ 65535) + 1 } + it "returns the exit status -27 as a signed integer or an unsigned 16-bit 2's complement value of 65509 for a powershell script that exits with -27" do + # Versions of PowerShell prior to 4.0 return a 16-bit unsigned value -- + # PowerShell 4.0 and later versions return a 32-bit signed value. + file = Tempfile.new(['foo', '.ps1']) + begin + file.write "exit #{negative_exit_status.to_s}" + file.close + resource.code(". \"#{file.path}\"") + + # PowerShell earlier than 4.0 takes negative exit codes + # and returns them as the underlying unsigned 16-bit + # 2's complement representation. We cover multiple versions + # of PowerShell in this example by including both the signed + # exit code and its converted counterpart as permitted return values. + # See http://support.microsoft.com/en-us/kb/2646183/zh-cn + resource.returns([negative_exit_status, unsigned_exit_status]) + expect { resource.run_action(:run) }.not_to raise_error + ensure + file.close + file.unlink + end + end it "returns the process exit code" do resource.code(arbitrary_nonzero_process_exit_code_content) @@ -98,7 +122,19 @@ describe Chef::Resource::WindowsScript::PowershellScript, :windows_only do it "returns 1 if the last command was a cmdlet that failed and was preceded by a successfully executed non-cmdlet Windows binary" do resource.code([windows_process_exit_code_success_content, cmdlet_exit_code_not_found_content].join(';')) resource.returns(1) - resource.run_action(:run) + expect { resource.run_action(:run) }.not_to raise_error + end + + it "raises a Mixlib::ShellOut::ShellCommandFailed error if the script is not syntactically correct" do + resource.code('if({)') + resource.returns(0) + expect { resource.run_action(:run) }.to raise_error(Mixlib::ShellOut::ShellCommandFailed) + end + + it "raises an error if the script is not syntactically correct even if returns is set to 1 which is what powershell.exe returns for syntactically invalid scripts" do + resource.code('if({)') + resource.returns(1) + expect { resource.run_action(:run) }.to raise_error(Mixlib::ShellOut::ShellCommandFailed) end # This somewhat ambiguous case, two failures of different types, @@ -191,10 +227,25 @@ describe Chef::Resource::WindowsScript::PowershellScript, :windows_only do expect { resource.should_skip?(:run) }.to raise_error(ArgumentError, /guard_interpreter does not support blocks/) end + context "when dsc is supported", :windows_powershell_dsc_only do + it "can execute LCM configuration code" do + resource.code <<-EOF +configuration LCM +{ + param ($thumbprint) + localconfigurationmanager + { + RebootNodeIfNeeded = $false + ConfigurationMode = 'ApplyOnly' + } +} + EOF + expect { resource.run_action(:run) }.not_to raise_error + end + end end - context "when running on a 32-bit version of Windows", :windows32_only do - + context "when running on a 32-bit version of Ruby", :ruby32_only do it "executes a script with a 32-bit process if process architecture :i386 is specified" do resource.code(processor_architecture_script_content + " | out-file -encoding ASCII #{script_output_path}") resource.architecture(:i386) @@ -204,15 +255,28 @@ describe Chef::Resource::WindowsScript::PowershellScript, :windows_only do expect(source_contains_case_insensitive_content?( get_script_output, 'x86' )).to eq(true) end - it "raises an exception if :x86_64 process architecture is specified" do - begin - expect(resource.architecture(:x86_64)).to raise_error Chef::Exceptions::Win32ArchitectureIncorrect - rescue Chef::Exceptions::Win32ArchitectureIncorrect + context "when running on a 64-bit version of Windows", :windows64_only do + it "executes a script with a 64-bit process if :x86_64 arch is specified" do + resource.code(processor_architecture_script_content + " | out-file -encoding ASCII #{script_output_path}") + resource.architecture(:x86_64) + resource.returns(0) + resource.run_action(:run) + + expect(source_contains_case_insensitive_content?( get_script_output, 'AMD64' )).to eq(true) + end + end + + context "when running on a 32-bit version of Windows", :windows32_only do + it "raises an exception if :x86_64 process architecture is specified" do + begin + expect(resource.architecture(:x86_64)).to raise_error Chef::Exceptions::Win32ArchitectureIncorrect + rescue Chef::Exceptions::Win32ArchitectureIncorrect + end end end end - context "when running on a 64-bit version of Windows", :windows64_only do + context "when running on a 64-bit version of Ruby", :ruby64_only do it "executes a script with a 64-bit process if :x86_64 arch is specified" do resource.code(processor_architecture_script_content + " | out-file -encoding ASCII #{script_output_path}") resource.architecture(:x86_64) diff --git a/spec/functional/resource/remote_directory_spec.rb b/spec/functional/resource/remote_directory_spec.rb index bcafca7399..37ffbbc971 100644 --- a/spec/functional/resource/remote_directory_spec.rb +++ b/spec/functional/resource/remote_directory_spec.rb @@ -22,7 +22,7 @@ describe Chef::Resource::RemoteDirectory do include_context Chef::Resource::Directory let(:directory_base) { "directory_spec" } - let(:default_mode) { ((0100777 - File.umask) & 07777).to_s(8) } + let(:default_mode) { (0777 & ~File.umask).to_s(8) } def create_resource cookbook_repo = File.expand_path(File.join(CHEF_SPEC_DATA, "cookbooks")) diff --git a/spec/functional/resource/remote_file_spec.rb b/spec/functional/resource/remote_file_spec.rb index 29091fd785..4fbcd2d24b 100644 --- a/spec/functional/resource/remote_file_spec.rb +++ b/spec/functional/resource/remote_file_spec.rb @@ -52,7 +52,7 @@ describe Chef::Resource::RemoteFile do create_resource end - let(:default_mode) { ((0100666 - File.umask) & 07777).to_s(8) } + let(:default_mode) { (0666 & ~File.umask).to_s(8) } context "when fetching files over HTTP" do before(:all) do diff --git a/spec/functional/resource/template_spec.rb b/spec/functional/resource/template_spec.rb index d7b35e7450..35c5166e31 100644 --- a/spec/functional/resource/template_spec.rb +++ b/spec/functional/resource/template_spec.rb @@ -58,7 +58,7 @@ describe Chef::Resource::Template do create_resource end - let(:default_mode) { ((0100666 - File.umask) & 07777).to_s(8) } + let(:default_mode) { (0666 & ~File.umask).to_s(8) } it_behaves_like "a file resource" diff --git a/spec/functional/resource/user/dscl_spec.rb b/spec/functional/resource/user/dscl_spec.rb index 45b6754453..5d960daf11 100644 --- a/spec/functional/resource/user/dscl_spec.rb +++ b/spec/functional/resource/user/dscl_spec.rb @@ -19,9 +19,8 @@ require 'spec_helper' require 'chef/mixin/shell_out' metadata = { - :unix_only => true, + :mac_osx_only => true, :requires_root => true, - :provider => {:user => Chef::Provider::User::Dscl}, :not_supported_on_mac_osx_106 => true, } diff --git a/spec/functional/resource/user/useradd_spec.rb b/spec/functional/resource/user/useradd_spec.rb index 9ac88d7b60..474f6a4ecf 100644 --- a/spec/functional/resource/user/useradd_spec.rb +++ b/spec/functional/resource/user/useradd_spec.rb @@ -32,6 +32,7 @@ end metadata = { :unix_only => true, :requires_root => true, + :not_supported_on_mac_osx => true, :provider => {:user => user_provider_for_platform} } @@ -64,8 +65,12 @@ describe Chef::Provider::User::Useradd, metadata do end end - def supports_quote_in_username? - OHAI_SYSTEM["platform_family"] == "debian" + def self.quote_in_username_unsupported? + if OHAI_SYSTEM["platform_family"] == "debian" + false + else + "Only debian family systems support quotes in username" + end end def password_should_be_set @@ -76,9 +81,22 @@ describe Chef::Provider::User::Useradd, metadata do end end + def try_cleanup + ['/home/cheftestfoo', '/home/cheftestbar'].each do |f| + FileUtils.rm_rf(f) if File.exists? f + end + + ['cf-test'].each do |u| + r = Chef::Resource::User.new("DELETE USER", run_context) + r.username('cf-test') + r.run_action(:remove) + end + end + before do # Silence shell_out live stream Chef::Log.level = :warn + try_cleanup end after do @@ -94,7 +112,7 @@ describe Chef::Provider::User::Useradd, metadata do break if status.exitstatus != 8 sleep 1 - max_retries = max_retries -1 + max_retries = max_retries - 1 rescue UserNotFound break end @@ -148,15 +166,10 @@ describe Chef::Provider::User::Useradd, metadata do end end - let(:skip) { false } - describe "action :create" do context "when the user does not exist beforehand" do before do - if reason = skip - pending(reason) - end user_resource.run_action(:create) expect(user_resource).to be_updated_by_last_action end @@ -172,14 +185,7 @@ describe Chef::Provider::User::Useradd, metadata do # tabulation: '\t', etc.). Note that using a slash ('/') may break the # default algorithm for the definition of the user's home directory. - context "and the username contains a single quote" do - let(:skip) do - if supports_quote_in_username? - false - else - "Platform #{OHAI_SYSTEM["platform"]} not expected to support username w/ quote" - end - end + context "and the username contains a single quote", skip: quote_in_username_unsupported? do let(:username) { "t'bilisi" } @@ -328,7 +334,7 @@ describe Chef::Provider::User::Useradd, metadata do before do if reason = skip - pending(reason) + skip(reason) end existing_user.run_action(:create) expect(existing_user).to be_updated_by_last_action @@ -386,18 +392,18 @@ describe Chef::Provider::User::Useradd, metadata do end context "and home directory is updated" do - let(:existing_home) { "/home/foo" } - let(:home) { "/home/bar" } + let(:existing_home) { "/home/cheftestfoo" } + let(:home) { "/home/cheftestbar" } it "ensures the home directory is set to the desired value" do - expect(pw_entry.home).to eq("/home/bar") + expect(pw_entry.home).to eq("/home/cheftestbar") end context "and manage_home is enabled" do let(:existing_manage_home) { true } let(:manage_home) { true } it "moves the home directory to the new location" do - expect(File).not_to exist("/home/foo") - expect(File).to exist("/home/bar") + expect(File).not_to exist("/home/cheftestfoo") + expect(File).to exist("/home/cheftestbar") end end @@ -409,19 +415,19 @@ describe Chef::Provider::User::Useradd, metadata do # Inconsistent behavior. See: CHEF-2205 it "created the home dir b/c of CHEF-2205 so it still exists" do # This behavior seems contrary to expectation and non-convergent. - expect(File).not_to exist("/home/foo") - expect(File).to exist("/home/bar") + expect(File).not_to exist("/home/cheftestfoo") + expect(File).to exist("/home/cheftestbar") end elsif ohai[:platform] == "aix" it "creates the home dir in the desired location" do - expect(File).not_to exist("/home/foo") - expect(File).to exist("/home/bar") + expect(File).not_to exist("/home/cheftestfoo") + expect(File).to exist("/home/cheftestbar") end else it "does not create the home dir in the desired location (XXX)" do # This behavior seems contrary to expectation and non-convergent. - expect(File).not_to exist("/home/foo") - expect(File).not_to exist("/home/bar") + expect(File).not_to exist("/home/cheftestfoo") + expect(File).not_to exist("/home/cheftestbar") end end end @@ -432,8 +438,8 @@ describe Chef::Provider::User::Useradd, metadata do it "leaves the old home directory around (XXX)" do # Would it be better to remove the old home? - expect(File).to exist("/home/foo") - expect(File).not_to exist("/home/bar") + expect(File).to exist("/home/cheftestfoo") + expect(File).not_to exist("/home/cheftestbar") end end end @@ -521,7 +527,7 @@ describe Chef::Provider::User::Useradd, metadata do def aix_user_lock_status lock_info = shell_out!("lsuser -a account_locked #{username}") - status = /\S+\s+account_locked=(\S+)/.match(lock_info.stdout)[1] + /\S+\s+account_locked=(\S+)/.match(lock_info.stdout)[1] end def user_account_should_be_locked diff --git a/spec/functional/resource/user/windows_spec.rb b/spec/functional/resource/user/windows_spec.rb new file mode 100644 index 0000000000..5e68478b34 --- /dev/null +++ b/spec/functional/resource/user/windows_spec.rb @@ -0,0 +1,125 @@ +# Author:: Jay Mundrawala (<jdm@chef.io>) +# Copyright:: Copyright (c) 2015 Chef Software +# 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/mixin/shell_out' + +describe Chef::Provider::User::Windows, :windows_only do + include Chef::Mixin::ShellOut + + let(:username) { 'ChefFunctionalTest' } + let(:password) { SecureRandom.uuid } + + let(:node) do + n = Chef::Node.new + n.consume_external_attrs(OHAI_SYSTEM.data.dup, {}) + n + end + + let(:events) { Chef::EventDispatch::Dispatcher.new } + let(:run_context) { Chef::RunContext.new(node, {}, events) } + let(:new_resource) do + Chef::Resource::User.new(username, run_context).tap do |r| + r.provider(Chef::Provider::User::Windows) + r.password(password) + end + end + + def delete_user(u) + shell_out("net user #{u} /delete") + end + + before do + delete_user(username) + end + + describe 'action :create' do + it 'creates a user when a username and password are given' do + new_resource.run_action(:create) + expect(new_resource).to be_updated_by_last_action + expect(shell_out("net user #{username}").exitstatus).to eq(0) + end + + it 'reports no changes if there are no changes needed' do + new_resource.run_action(:create) + new_resource.run_action(:create) + expect(new_resource).not_to be_updated_by_last_action + end + + it 'allows chaning the password' do + new_resource.run_action(:create) + new_resource.password(SecureRandom.uuid) + new_resource.run_action(:create) + expect(new_resource).to be_updated_by_last_action + end + end + + describe 'action :remove' do + before do + new_resource.run_action(:create) + end + + it 'deletes the user' do + new_resource.run_action(:remove) + expect(new_resource).to be_updated_by_last_action + expect(shell_out("net user #{username}").exitstatus).to eq(2) + end + + it 'is idempotent' do + new_resource.run_action(:remove) + new_resource.run_action(:remove) + expect(new_resource).not_to be_updated_by_last_action + end + end + + describe 'action :lock' do + before do + new_resource.run_action(:create) + end + + it 'locks the user account' do + new_resource.run_action(:lock) + expect(new_resource).to be_updated_by_last_action + expect(shell_out("net user #{username}").stdout).to match(/Account active\s*No/) + end + + it 'is idempotent' do + new_resource.run_action(:lock) + new_resource.run_action(:lock) + expect(new_resource).not_to be_updated_by_last_action + end + end + + describe 'action :unlock' do + before do + new_resource.run_action(:create) + new_resource.run_action(:lock) + end + + it 'unlocks the user account' do + new_resource.run_action(:unlock) + expect(new_resource).to be_updated_by_last_action + expect(shell_out("net user #{username}").stdout).to match(/Account active\s*Yes/) + end + + it 'is idempotent' do + new_resource.run_action(:unlock) + new_resource.run_action(:unlock) + expect(new_resource).not_to be_updated_by_last_action + end + end +end diff --git a/spec/functional/shell_spec.rb b/spec/functional/shell_spec.rb index fa9de77b0e..a753948c7f 100644 --- a/spec/functional/shell_spec.rb +++ b/spec/functional/shell_spec.rb @@ -29,6 +29,8 @@ describe Shell do describe "smoke tests", :unix_only => true do include Chef::Mixin::Command::Unix + TIMEOUT=300 + def read_until(io, expected_value) start = Time.new buffer = "" @@ -38,15 +40,30 @@ describe Shell do rescue Errno::EWOULDBLOCK, Errno::EAGAIN, Errno::EIO, EOFError sleep 0.01 end - if Time.new - start > 30 - STDERR.puts "did not read expected value `#{expected_value}' within 15s" - STDERR.puts "Buffer so far: `#{buffer}'" - break + if Time.new - start > TIMEOUT + raise "did not read expected value `#{expected_value}' within #{TIMEOUT}s\n" + + "Buffer so far: `#{buffer}'" end end buffer end + def flush_output(io) + start = Time.new + loop do + begin + io.read_nonblock(1) + rescue Errno::EWOULDBLOCK, Errno::EAGAIN + sleep 0.01 + rescue EOFError, Errno::EIO + break + end + if Time.new - start > TIMEOUT + raise "timed out after #{TIMEOUT}s waiting for output to end" + end + end + end + def wait_or_die(pid) start = Time.new @@ -67,12 +84,12 @@ describe Shell do path_to_chef_shell = File.expand_path("../../../bin/chef-shell", __FILE__) output = '' status = popen4("#{path_to_chef_shell} -c #{config} #{options}", :waitlast => true) do |pid, stdin, stdout, stderr| - read_until(stdout, "chef >") + read_until(stdout, "chef (#{Chef::VERSION})>") yield stdout, stdin if block_given? stdin.write("'done'\n") output = read_until(stdout, '=> "done"') stdin.print("exit\n") - read_until(stdout, "\n") + flush_output(stdout) end [output, status.exitstatus] @@ -84,14 +101,12 @@ describe Shell do config = File.expand_path("shef-config.rb", CHEF_SPEC_DATA) path_to_chef_shell = File.expand_path("../../../bin/chef-shell", __FILE__) reader, writer, pid = PTY.spawn("#{path_to_chef_shell} -c #{config} #{options}") - read_until(reader, "chef >") + read_until(reader, "chef (#{Chef::VERSION})>") yield reader, writer if block_given? writer.puts('"done"') output = read_until(reader, '=> "done"') writer.print("exit\n") - read_until(reader, "exit") - read_until(reader, "\n") - read_until(reader, "\n") + flush_output(reader) writer.close exitstatus = wait_or_die(pid) diff --git a/spec/functional/util/powershell/cmdlet_spec.rb b/spec/functional/util/powershell/cmdlet_spec.rb index b240a5ec12..201fb95af8 100644 --- a/spec/functional/util/powershell/cmdlet_spec.rb +++ b/spec/functional/util/powershell/cmdlet_spec.rb @@ -19,7 +19,7 @@ require 'chef/json_compat' require File.expand_path('../../../../spec_helper', __FILE__) -describe Chef::Util::Powershell::Cmdlet, :windows_only do +describe Chef::Util::Powershell::Cmdlet, :windows_powershell_dsc_only do before(:all) do ohai = Ohai::System.new ohai.load_plugins @@ -88,7 +88,7 @@ describe Chef::Util::Powershell::Cmdlet, :windows_only do context "when returning json" do let(:cmd_output_format) { :json } - it "returns json format data", :windows_powershell_dsc_only do + it "returns json format data" do result = cmdlet_alias_requires_switch_or_argument.run({},{},'ls') expect(result.succeeded?).to eq(true) expect(lambda{Chef::JSONCompat.parse(result.return_value)}).not_to raise_error @@ -97,7 +97,7 @@ describe Chef::Util::Powershell::Cmdlet, :windows_only do context "when returning Ruby objects" do let(:cmd_output_format) { :object } - it "returns object format data", :windows_powershell_dsc_only do + it "returns object format data" do result = simple_cmdlet.run({},{:cwd => etc_directory}, 'hosts') expect(result.succeeded?).to eq(true) data = result.return_value diff --git a/spec/functional/win32/crypto_spec.rb b/spec/functional/win32/crypto_spec.rb new file mode 100644 index 0000000000..1492995886 --- /dev/null +++ b/spec/functional/win32/crypto_spec.rb @@ -0,0 +1,57 @@ +# +# Author:: Jay Mundrawala(<jdm@chef.io>) +# 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' +if Chef::Platform.windows? + require 'chef/win32/crypto' +end + +describe 'Chef::ReservedNames::Win32::Crypto', :windows_only do + describe '#encrypt' do + before(:all) do + ohai_reader = Ohai::System.new + ohai_reader.all_plugins("platform") + + new_node = Chef::Node.new + new_node.consume_external_attrs(ohai_reader.data,{}) + + events = Chef::EventDispatch::Dispatcher.new + + @run_context = Chef::RunContext.new(new_node, {}, events) + end + + let (:plaintext) { 'p@assword' } + + it 'can be decrypted by powershell' do + encrypted = Chef::ReservedNames::Win32::Crypto.encrypt(plaintext) + resource = Chef::Resource::WindowsScript::PowershellScript.new("Powershell resource functional test", @run_context) + resource.code <<-EOF +$encrypted = '#{encrypted}' | ConvertTo-SecureString +$BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($encrypted) +$plaintext = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) +if ($plaintext -ne '#{plaintext}') { + Write-Error 'Got: ' $plaintext + exit 1 +} +exit 0 + EOF + resource.returns(0) + resource.run_action(:run) + end + end +end diff --git a/spec/functional/win32/registry_helper_spec.rb b/spec/functional/win32/registry_helper_spec.rb index 7b070e6fe1..9ef6fd006f 100644 --- a/spec/functional/win32/registry_helper_spec.rb +++ b/spec/functional/win32/registry_helper_spec.rb @@ -130,6 +130,9 @@ describe 'Chef::Win32::Registry', :windows_only do it "returns true if the value exists" do expect(@registry.value_exists?("HKCU\\Software\\Root\\Branch\\Flower", {:name=>"Petals"})).to eq(true) end + it "returns true if the value exists with a case mismatch on the value name" do + expect(@registry.value_exists?("HKCU\\Software\\Root\\Branch\\Flower", {:name=>"petals"})).to eq(true) + end it "returns false if the value does not exist" do expect(@registry.value_exists?("HKCU\\Software\\Root\\Branch\\Flower", {:name=>"FOOBAR"})).to eq(false) end @@ -145,6 +148,9 @@ describe 'Chef::Win32::Registry', :windows_only do it "returns true if the value exists" do expect(@registry.value_exists!("HKCU\\Software\\Root\\Branch\\Flower", {:name=>"Petals"})).to eq(true) end + it "returns true if the value exists with a case mismatch on the value name" do + expect(@registry.value_exists!("HKCU\\Software\\Root\\Branch\\Flower", {:name=>"petals"})).to eq(true) + end it "throws an exception if the value does not exist" do expect {@registry.value_exists!("HKCU\\Software\\Root\\Branch\\Flower", {:name=>"FOOBAR"})}.to raise_error(Chef::Exceptions::Win32RegValueMissing) end @@ -160,6 +166,9 @@ describe 'Chef::Win32::Registry', :windows_only do it "returns true if all the data matches" do expect(@registry.data_exists?("HKCU\\Software\\Root\\Branch\\Flower", {:name=>"Petals", :type=>:multi_string, :data=>["Pink", "Delicate"]})).to eq(true) end + it "returns true if all the data matches with a case mismatch on the data name" do + expect(@registry.data_exists?("HKCU\\Software\\Root\\Branch\\Flower", {:name=>"petals", :type=>:multi_string, :data=>["Pink", "Delicate"]})).to eq(true) + end it "returns false if the name does not exist" do expect(@registry.data_exists?("HKCU\\Software\\Root\\Branch\\Flower", {:name=>"slateP", :type=>:multi_string, :data=>["Pink", "Delicate"]})).to eq(false) end @@ -181,6 +190,9 @@ describe 'Chef::Win32::Registry', :windows_only do it "returns true if all the data matches" do expect(@registry.data_exists!("HKCU\\Software\\Root\\Branch\\Flower", {:name=>"Petals", :type=>:multi_string, :data=>["Pink", "Delicate"]})).to eq(true) end + it "returns true if all the data matches with a case mismatch on the data name" do + expect(@registry.data_exists!("HKCU\\Software\\Root\\Branch\\Flower", {:name=>"petals", :type=>:multi_string, :data=>["Pink", "Delicate"]})).to eq(true) + end it "throws an exception if the name does not exist" do expect {@registry.data_exists!("HKCU\\Software\\Root\\Branch\\Flower", {:name=>"slateP", :type=>:multi_string, :data=>["Pink", "Delicate"]})}.to raise_error(Chef::Exceptions::Win32RegDataMissing) end diff --git a/spec/functional/win32/service_manager_spec.rb b/spec/functional/win32/service_manager_spec.rb index d2474deace..a1ce36146f 100644 --- a/spec/functional/win32/service_manager_spec.rb +++ b/spec/functional/win32/service_manager_spec.rb @@ -33,7 +33,7 @@ end # directories. # -describe "Chef::Application::WindowsServiceManager", :windows_only, :system_windows_service_gem_only do +describe "Chef::Application::WindowsServiceManager", :windows_only, :system_windows_service_gem_only, :appveyor_only do include_context "using Win32::Service" @@ -43,7 +43,7 @@ describe "Chef::Application::WindowsServiceManager", :windows_only, :system_wind end it "throws an error with required missing options" do - test_service.each do |key,value| + [:service_name, :service_display_name, :service_description, :service_file_path].each do |key| service_def = test_service.dup service_def.delete(key) diff --git a/spec/functional/win32/sid_spec.rb b/spec/functional/win32/sid_spec.rb new file mode 100644 index 0000000000..1f5f66178a --- /dev/null +++ b/spec/functional/win32/sid_spec.rb @@ -0,0 +1,55 @@ +# +# Author:: Dan Bjorge (<dbjorge@gmail.com>) +# Copyright:: Copyright (c) 2015 Dan Bjorge +# 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' +if Chef::Platform.windows? + require 'chef/win32/security' +end + +describe 'Chef::ReservedNames::Win32::SID', :windows_only do + if Chef::Platform.windows? + SID ||= Chef::ReservedNames::Win32::Security::SID + end + + it 'should resolve default_security_object_group as a sane user group', :windows_not_domain_joined_only do + # Domain accounts: domain-specific Domain Users SID + # Microsoft Accounts: SID.current_user + # Else: SID.None + expect(SID.default_security_object_group).to eq(SID.None).or eq(SID.current_user) + end + + context 'running as an elevated administrator user' do + it 'should resolve default_security_object_owner as the Administrators group' do + expect(SID.default_security_object_owner).to eq(SID.Administrators) + end + end + + context 'running as a non-elevated administrator user' do + it 'should resolve default_security_object_owner as the current user' do + skip 'requires user support in mixlib-shellout, see security_spec.rb' + expect(SID.default_security_object_owner).to eq(SID.Administrators) + end + end + + context 'running as a non-elevated, non-administrator user' do + it 'should resolve default_security_object_owner as the current user' do + skip 'requires user support in mixlib-shellout, see security_spec.rb' + expect(SID.default_security_object_owner).to eq(SID.current_user) + end + end +end diff --git a/spec/integration/client/client_spec.rb b/spec/integration/client/client_spec.rb index 8afb52e29a..1a030c130b 100644 --- a/spec/integration/client/client_spec.rb +++ b/spec/integration/client/client_spec.rb @@ -3,34 +3,35 @@ require 'chef/mixin/shell_out' require 'tiny_server' require 'tmpdir' -def recipes_filename - File.join(CHEF_SPEC_DATA, 'recipes.tgz') -end -def start_tiny_server(server_opts={}) - recipes_size = File::Stat.new(recipes_filename).size - @server = TinyServer::Manager.new(server_opts) - @server.start - @api = TinyServer::API.instance - @api.clear - # - # trivial endpoints - # - # just a normal file - # (expected_content should be uncompressed) - @api.get("/recipes.tgz", 200) { - File.open(recipes_filename, "rb") do |f| - f.read - end - } -end +describe "chef-client" do -def stop_tiny_server - @server.stop - @server = @api = nil -end + def recipes_filename + File.join(CHEF_SPEC_DATA, 'recipes.tgz') + end + + def start_tiny_server(server_opts={}) + @server = TinyServer::Manager.new(server_opts) + @server.start + @api = TinyServer::API.instance + @api.clear + # + # trivial endpoints + # + # just a normal file + # (expected_content should be uncompressed) + @api.get("/recipes.tgz", 200) { + File.open(recipes_filename, "rb") do |f| + f.read + end + } + end + + def stop_tiny_server + @server.stop + @server = @api = nil + end -describe "chef-client" do include IntegrationSupport include Chef::Mixin::ShellOut @@ -45,7 +46,9 @@ describe "chef-client" do # machine that has omnibus chef installed. In that case we need to ensure # we're running `chef-client` from the source tree and not the external one. # cf. CHEF-4914 - let(:chef_client) { "ruby '#{chef_dir}/chef-client'" } + let(:chef_client) { "ruby '#{chef_dir}/chef-client' --minimal-ohai" } + + let(:critical_env_vars) { %w(PATH RUBYOPT BUNDLE_GEMFILE GEM_PATH).map {|o| "#{o}=#{ENV[o]}"} .join(' ') } when_the_repository "has a cookbook with a no-op recipe" do before { file 'cookbooks/x/recipes/default.rb', '' } @@ -56,7 +59,38 @@ local_mode true cookbook_path "#{path_to('cookbooks')}" EOM - result = shell_out("#{chef_client} -c \"#{path_to('config/client.rb')}\" -o 'x::default'", :cwd => chef_dir) + shell_out!("#{chef_client} -c \"#{path_to('config/client.rb')}\" -o 'x::default'", :cwd => chef_dir) + end + + it "should complete successfully with no other environment variables", :skip => (Chef::Platform.windows?) do + file 'config/client.rb', <<EOM +local_mode true +cookbook_path "#{path_to('cookbooks')}" +EOM + + begin + result = shell_out("env -i #{critical_env_vars} #{chef_client} -c \"#{path_to('config/client.rb')}\" -o 'x::default'", :cwd => chef_dir) + result.error! + rescue + Chef::Log.info "Bare invocation will have the following load-path." + Chef::Log.info shell_out!("env -i #{critical_env_vars} ruby -e 'puts $:'").stdout + raise + end + end + + it "should complete successfully with --no-listen" do + file 'config/client.rb', <<EOM +local_mode true +cookbook_path "#{path_to('cookbooks')}" +EOM + + result = shell_out("#{chef_client} --no-listen -c \"#{path_to('config/client.rb')}\" -o 'x::default'", :cwd => chef_dir) + result.error! + end + + it "should be able to node.save with bad utf8 characters in the node data" do + file "cookbooks/x/attributes/default.rb", 'default["badutf8"] = "Elan Ruusam\xE4e"' + result = shell_out("#{chef_client} -z -r 'x::default' --disable-config", :cwd => path_to('')) result.error! end @@ -269,6 +303,59 @@ EOM end + when_the_repository "has a cookbook that generates deprecation warnings" do + before do + file 'cookbooks/x/recipes/default.rb', <<-EOM + class ::MyResource < Chef::Resource + use_automatic_resource_name + property :x, default: [] + property :y, default: {} + end + + my_resource 'blah' do + 1.upto(10) do + x nil + end + x nil + end + EOM + end + + def match_indices(regex, str) + result = [] + pos = 0 + while match = regex.match(str, pos) + result << match.begin(0) + pos = match.end(0) + 1 + end + result + end + + it "should output each deprecation warning only once, at the end of the run" do + file 'config/client.rb', <<EOM +local_mode true +cookbook_path "#{path_to('cookbooks')}" +# Mimick what happens when you are on the console +formatters << :doc +log_level :warn +EOM + + ENV.delete('CHEF_TREAT_DEPRECATION_WARNINGS_AS_ERRORS') + + result = shell_out!("#{chef_client} -c \"#{path_to('config/client.rb')}\" -o 'x::default'", :cwd => chef_dir) + expect(result.error?).to be_falsey + + # Search to the end of the client run in the output + run_complete = result.stdout.index("Running handlers complete") + expect(run_complete).to be >= 0 + + # Make sure there is exactly one result for each, and that it occurs *after* the complete message. + expect(match_indices(/MyResource.x has an array or hash default/, result.stdout)).to match([ be > run_complete ]) + expect(match_indices(/MyResource.y has an array or hash default/, result.stdout)).to match([ be > run_complete ]) + expect(match_indices(/nil currently does not overwrite the value of/, result.stdout)).to match([ be > run_complete ]) + end + end + when_the_repository "has a cookbook with only an audit recipe" do before do @@ -308,7 +395,8 @@ end end end - context "when using recipe-url" do + # Fails on appveyor, but works locally on windows and on windows hosts in Ci. + context "when using recipe-url", :skip_appveyor do before(:all) do start_tiny_server end @@ -329,7 +417,7 @@ EOM it 'should fail when passed --recipe-url and not passed -z' do result = shell_out("#{chef_client} --recipe-url=http://localhost:9000/recipes.tgz", :cwd => tmp_dir) - expect(result.exitstatus).to eq(1) + expect(result.exitstatus).not_to eq(0) end end end diff --git a/spec/integration/client/ipv6_spec.rb b/spec/integration/client/ipv6_spec.rb index 76dd1938f7..8be873edf4 100644 --- a/spec/integration/client/ipv6_spec.rb +++ b/spec/integration/client/ipv6_spec.rb @@ -76,7 +76,7 @@ END_CLIENT_RB let(:chef_dir) { File.join(File.dirname(__FILE__), "..", "..", "..", "bin") } - let(:chef_client_cmd) { %Q[ruby '#{chef_dir}/chef-client' -c "#{path_to('config/client.rb')}" -lwarn] } + let(:chef_client_cmd) { %Q[ruby '#{chef_dir}/chef-client' --minimal-ohai -c "#{path_to('config/client.rb')}" -lwarn] } after do FileUtils.rm_rf(cache_path) diff --git a/spec/integration/knife/chef_repo_path_spec.rb b/spec/integration/knife/chef_repo_path_spec.rb index 874b33901f..908657e5f7 100644 --- a/spec/integration/knife/chef_repo_path_spec.rb +++ b/spec/integration/knife/chef_repo_path_spec.rb @@ -24,6 +24,8 @@ describe 'chef_repo_path tests', :workstation do include IntegrationSupport include KnifeSupport + let(:error_rel_path_outside_repo) { /^ERROR: Attempt to use relative path '' when current directory is outside the repository path/ } + # TODO alternate repo_path / *_path context 'alternate *_path' do when_the_repository 'has clients and clients2, cookbooks and cookbooks2, etc.' do @@ -109,14 +111,14 @@ EOM context 'when cwd is at the top level' do before { cwd '.' } it 'knife list --local -Rfp fails' do - knife('list --local -Rfp').should_fail("ERROR: Attempt to use relative path '' when current directory is outside the repository path\n") + knife('list --local -Rfp').should_fail(error_rel_path_outside_repo) end end context 'when cwd is inside the data_bags directory' do before { cwd 'data_bags' } it 'knife list --local -Rfp fails' do - knife('list --local -Rfp').should_fail("ERROR: Attempt to use relative path '' when current directory is outside the repository path\n") + knife('list --local -Rfp').should_fail(error_rel_path_outside_repo) end end @@ -192,14 +194,14 @@ EOM context 'when cwd is inside the data_bags directory' do before { cwd 'data_bags' } it 'knife list --local -Rfp fails' do - knife('list --local -Rfp').should_fail("ERROR: Attempt to use relative path '' when current directory is outside the repository path\n") + knife('list --local -Rfp').should_fail(error_rel_path_outside_repo) end end context 'when cwd is inside chef_repo2' do before { cwd 'chef_repo2' } it 'knife list -Rfp fails' do - knife('list --local -Rfp').should_fail("ERROR: Attempt to use relative path '' when current directory is outside the repository path\n") + knife('list --local -Rfp').should_fail(error_rel_path_outside_repo) end end @@ -225,14 +227,14 @@ EOM context 'when cwd is at the top level' do before { cwd '.' } it 'knife list --local -Rfp fails' do - knife('list --local -Rfp').should_fail("ERROR: Attempt to use relative path '' when current directory is outside the repository path\n") + knife('list --local -Rfp').should_fail(error_rel_path_outside_repo) end end context 'when cwd is inside the data_bags directory' do before { cwd 'data_bags' } it 'knife list --local -Rfp fails' do - knife('list --local -Rfp').should_fail("ERROR: Attempt to use relative path '' when current directory is outside the repository path\n") + knife('list --local -Rfp').should_fail(error_rel_path_outside_repo) end end @@ -445,7 +447,7 @@ EOM context 'when cwd is at the top level' do before { cwd '.' } it 'knife list --local -Rfp fails' do - knife('list --local -Rfp').should_fail("ERROR: Attempt to use relative path '' when current directory is outside the repository path\n") + knife('list --local -Rfp').should_fail(error_rel_path_outside_repo) end end @@ -621,14 +623,14 @@ EOM context 'when cwd is at the top level' do before { cwd '.' } it 'knife list --local -Rfp fails' do - knife('list --local -Rfp').should_fail("ERROR: Attempt to use relative path '' when current directory is outside the repository path\n") + knife('list --local -Rfp').should_fail(error_rel_path_outside_repo) end end context 'when cwd is inside the data_bags directory' do before { cwd 'data_bags' } it 'knife list --local -Rfp fails' do - knife('list --local -Rfp').should_fail("ERROR: Attempt to use relative path '' when current directory is outside the repository path\n") + knife('list --local -Rfp').should_fail(error_rel_path_outside_repo) end end @@ -782,7 +784,7 @@ EOM context 'when cwd is at the top level' do before { cwd '.' } it 'knife list --local -Rfp fails' do - knife('list --local -Rfp').should_fail("ERROR: Attempt to use relative path '' when current directory is outside the repository path\n") + knife('list --local -Rfp').should_fail(error_rel_path_outside_repo) end end @@ -823,7 +825,7 @@ EOM context 'when cwd is inside chef_repo2/data_bags' do before { cwd 'chef_repo2/data_bags' } it 'knife list --local -Rfp fails' do - knife('list --local -Rfp').should_fail("ERROR: Attempt to use relative path '' when current directory is outside the repository path\n") + knife('list --local -Rfp').should_fail(error_rel_path_outside_repo) end end end diff --git a/spec/integration/knife/common_options_spec.rb b/spec/integration/knife/common_options_spec.rb index ec76738b6f..b2e2e3fc2a 100644 --- a/spec/integration/knife/common_options_spec.rb +++ b/spec/integration/knife/common_options_spec.rb @@ -39,7 +39,7 @@ describe 'knife common options', :workstation do it 'knife raw /nodes/x should retrieve the node' do knife('raw /nodes/x').should_succeed( /"name": "x"/ ) - expect(Chef::Config.chef_server_url).to eq('http://localhost:9999') + expect(Chef::Config.chef_server_url).to eq('chefzero://localhost:9999') end end @@ -101,7 +101,7 @@ EOM it 'knife raw -z --chef-zero-port=9999 /nodes/x retrieves the node' do knife('raw -z --chef-zero-port=9999 /nodes/x').should_succeed( /"name": "x"/ ) - expect(Chef::Config.chef_server_url).to eq('http://localhost:9999') + expect(Chef::Config.chef_server_url).to eq('chefzero://localhost:9999') end context 'when the default port (8889) is already bound' do @@ -149,7 +149,7 @@ EOM it 'knife raw -z --chef-zero-port=9999 /nodes/x retrieves the node' do knife('raw -z --chef-zero-port=9999 /nodes/x').should_succeed( /"name": "x"/ ) - expect(Chef::Config.chef_server_url).to eq('http://localhost:9999') + expect(Chef::Config.chef_server_url).to eq('chefzero://localhost:9999') end end end diff --git a/spec/integration/knife/deps_spec.rb b/spec/integration/knife/deps_spec.rb index 3120db4940..b7333cefda 100644 --- a/spec/integration/knife/deps_spec.rb +++ b/spec/integration/knife/deps_spec.rb @@ -216,22 +216,16 @@ depends "self"' end it 'knife deps prints each once' do - knife('deps /cookbooks/foo /cookbooks/self').should_succeed <<EOM -/cookbooks/baz -/cookbooks/bar -/cookbooks/foo -/cookbooks/self -EOM + knife('deps /cookbooks/foo /cookbooks/self').should_succeed( + stdout: "/cookbooks/baz\n/cookbooks/bar\n/cookbooks/foo\n/cookbooks/self\n", + stderr: "WARN: Ignoring self-dependency in cookbook self, please remove it (in the future this will be fatal).\n" + ) end it 'knife deps --tree prints each once' do - knife('deps --tree /cookbooks/foo /cookbooks/self').should_succeed <<EOM -/cookbooks/foo - /cookbooks/bar - /cookbooks/baz - /cookbooks/foo -/cookbooks/self - /cookbooks/self -EOM + knife('deps --tree /cookbooks/foo /cookbooks/self').should_succeed( + stdout: "/cookbooks/foo\n /cookbooks/bar\n /cookbooks/baz\n /cookbooks/foo\n/cookbooks/self\n", + stderr: "WARN: Ignoring self-dependency in cookbook self, please remove it (in the future this will be fatal).\n" + ) end end when_the_repository 'has roles with circular dependencies' do diff --git a/spec/integration/knife/upload_spec.rb b/spec/integration/knife/upload_spec.rb index cef4f54e97..826ecec364 100644 --- a/spec/integration/knife/upload_spec.rb +++ b/spec/integration/knife/upload_spec.rb @@ -154,6 +154,24 @@ EOM end end + context 'when cookbook metadata has a self-dependency' do + before do + file 'cookbooks/x/metadata.rb', "name 'x'; version '1.0.0'; depends 'x'" + end + + it "should warn", :chef_lt_13_only do + knife('upload /cookbooks').should_succeed( + stdout: "Updated /cookbooks/x\n", + stderr: "WARN: Ignoring self-dependency in cookbook x, please remove it (in the future this will be fatal).\n" + ) + knife('diff --name-status /').should_succeed '' + end + it "should fail in Chef 13", :chef_gte_13_only do + knife('upload /cookbooks').should_fail '' + # FIXME: include the error message here + end + end + context 'as well as one extra copy of each thing' do before do file 'clients/y.json', { 'public_key' => ChefZero::PUBLIC_KEY } diff --git a/spec/integration/recipes/lwrp_inline_resources_spec.rb b/spec/integration/recipes/lwrp_inline_resources_spec.rb index a3baba8b0f..e70605d3d3 100644 --- a/spec/integration/recipes/lwrp_inline_resources_spec.rb +++ b/spec/integration/recipes/lwrp_inline_resources_spec.rb @@ -5,7 +5,7 @@ describe "LWRPs with inline resources" do include IntegrationSupport include Chef::Mixin::ShellOut - let(:chef_dir) { File.join(File.dirname(__FILE__), "..", "..", "..", "bin") } + let(:chef_dir) { File.expand_path("../../../../bin", __FILE__) } # Invoke `chef-client` as `ruby PATH/TO/chef-client`. This ensures the # following constraints are satisfied: @@ -16,7 +16,7 @@ describe "LWRPs with inline resources" do # machine that has omnibus chef installed. In that case we need to ensure # we're running `chef-client` from the source tree and not the external one. # cf. CHEF-4914 - let(:chef_client) { "ruby '#{chef_dir}/chef-client'" } + let(:chef_client) { "ruby '#{chef_dir}/chef-client' --minimal-ohai" } when_the_repository "has a cookbook with a nested LWRP" do before do diff --git a/spec/integration/recipes/lwrp_spec.rb b/spec/integration/recipes/lwrp_spec.rb new file mode 100644 index 0000000000..7ecdfc7c3a --- /dev/null +++ b/spec/integration/recipes/lwrp_spec.rb @@ -0,0 +1,53 @@ +require 'support/shared/integration/integration_helper' +require 'chef/mixin/shell_out' + +describe "LWRPs" do + include IntegrationSupport + include Chef::Mixin::ShellOut + + let(:chef_dir) { File.expand_path("../../../../bin", __FILE__) } + + # Invoke `chef-client` as `ruby PATH/TO/chef-client`. This ensures the + # following constraints are satisfied: + # * Windows: windows can only run batch scripts as bare executables. Rubygems + # creates batch wrappers for installed gems, but we don't have batch wrappers + # in the source tree. + # * Other `chef-client` in PATH: A common case is running the tests on a + # machine that has omnibus chef installed. In that case we need to ensure + # we're running `chef-client` from the source tree and not the external one. + # cf. CHEF-4914 + let(:chef_client) { "ruby '#{chef_dir}/chef-client' --minimal-ohai" } + + when_the_repository "has a cookbook named l-w-r-p" do + before do + directory 'cookbooks/l-w-r-p' do + + file 'resources/foo.rb', <<EOM +default_action :create +EOM + file 'providers/foo.rb', <<EOM +action :create do +end +EOM + + file 'recipes/default.rb', <<EOM +l_w_r_p_foo "me" +EOM + + end # directory 'cookbooks/x' + end + + it "should complete with success" do + file 'config/client.rb', <<EOM +local_mode true +cookbook_path "#{path_to('cookbooks')}" +log_level :warn +EOM + + result = shell_out("#{chef_client} -c \"#{path_to('config/client.rb')}\" --no-color -F doc -o 'l-w-r-p::default'", :cwd => chef_dir) + expect(result.stdout).to match(/\* l_w_r_p_foo\[me\] action create \(up to date\)/) + expect(result.stdout).not_to match(/WARN: You are overriding l_w_r_p_foo/) + result.error! + end + end +end diff --git a/spec/integration/recipes/provider_choice.rb b/spec/integration/recipes/provider_choice.rb new file mode 100644 index 0000000000..01537b2c05 --- /dev/null +++ b/spec/integration/recipes/provider_choice.rb @@ -0,0 +1,36 @@ +require 'support/shared/integration/integration_helper' + +describe "Recipe DSL methods" do + include IntegrationSupport + + context "With resource class providing 'provider_thingy'" do + before :context do + class Chef::Resource::ProviderThingy < Chef::Resource + resource_name :provider_thingy + default_action :create + def to_s + "provider_thingy resource class" + end + end + end + context "And class Chef::Provider::ProviderThingy with no provides" do + before :context do + class Chef::Provider::ProviderThingy < Chef::Provider + def load_current_resource + end + def action_create + Chef::Log.warn("hello from #{self.class.name}") + end + end + end + + it "provider_thingy 'blah' runs the provider and warns" do + recipe = converge { + provider_thingy 'blah' do; end + } + expect(recipe.logged_warnings).to match /hello from Chef::Provider::ProviderThingy/ + expect(recipe.logged_warnings).to match /you must use 'provides' to provide DSL/i + end + end + end +end diff --git a/spec/integration/recipes/recipe_dsl_spec.rb b/spec/integration/recipes/recipe_dsl_spec.rb new file mode 100644 index 0000000000..52bca87c99 --- /dev/null +++ b/spec/integration/recipes/recipe_dsl_spec.rb @@ -0,0 +1,1492 @@ +require 'support/shared/integration/integration_helper' + +describe "Recipe DSL methods" do + include IntegrationSupport + + module Namer + extend self + attr_accessor :current_index + end + + before(:all) { Namer.current_index = 1 } + before { Namer.current_index += 1 } + + context "with resource 'base_thingy' declared as BaseThingy" do + before(:context) { + + class BaseThingy < Chef::Resource + resource_name 'base_thingy' + default_action :create + + class<<self + attr_accessor :created_name + attr_accessor :created_resource + attr_accessor :created_provider + end + + def provider + Provider + end + class Provider < Chef::Provider + def load_current_resource + end + def action_create + BaseThingy.created_name = new_resource.name + BaseThingy.created_resource = new_resource.class + BaseThingy.created_provider = self.class + end + end + end + + # Modules to put stuff in + module RecipeDSLSpecNamespace; end + module RecipeDSLSpecNamespace::Bar; end + + } + + before :each do + BaseThingy.created_resource = nil + BaseThingy.created_provider = nil + end + + it "creates base_thingy when you call base_thingy in a recipe" do + recipe = converge { + base_thingy 'blah' do; end + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_name).to eq 'blah' + expect(BaseThingy.created_resource).to eq BaseThingy + end + + it "errors out when you call base_thingy do ... end in a recipe" do + expect_converge { + base_thingy do; end + }.to raise_error(ArgumentError, 'You must supply a name when declaring a base_thingy resource') + end + + it "emits a warning when you call base_thingy 'foo', 'bar' do ... end in a recipe" do + Chef::Config[:treat_deprecation_warnings_as_errors] = false + recipe = converge { + base_thingy 'foo', 'bar' do + end + } + expect(recipe.logged_warnings).to match(/Cannot create resource base_thingy with more than one argument. All arguments except the name \("foo"\) will be ignored. This will cause an error in Chef 13. Arguments: \["foo", "bar"\]/) + expect(BaseThingy.created_name).to eq 'foo' + expect(BaseThingy.created_resource).to eq BaseThingy + end + + context "Deprecated automatic resource DSL" do + before do + Chef::Config[:treat_deprecation_warnings_as_errors] = false + end + + context "with a resource 'backcompat_thingy' declared in Chef::Resource and Chef::Provider" do + before(:context) { + + class Chef::Resource::BackcompatThingy < Chef::Resource + default_action :create + end + class Chef::Provider::BackcompatThingy < Chef::Provider + def load_current_resource + end + def action_create + BaseThingy.created_resource = new_resource.class + BaseThingy.created_provider = self.class + end + end + + } + + it "backcompat_thingy creates a Chef::Resource::BackcompatThingy" do + recipe = converge { + backcompat_thingy 'blah' do; end + } + expect(BaseThingy.created_resource).to eq Chef::Resource::BackcompatThingy + expect(BaseThingy.created_provider).to eq Chef::Provider::BackcompatThingy + end + + context "and another resource 'backcompat_thingy' in BackcompatThingy with 'provides'" do + before(:context) { + + class RecipeDSLSpecNamespace::BackcompatThingy < BaseThingy + provides :backcompat_thingy + resource_name :backcompat_thingy + end + + } + + it "backcompat_thingy creates a BackcompatThingy" do + recipe = converge { + backcompat_thingy 'blah' do; end + } + expect(recipe.logged_warnings).to match(/Class Chef::Provider::BackcompatThingy does not declare 'provides :backcompat_thingy'./) + expect(BaseThingy.created_resource).not_to be_nil + end + end + end + + context "with a resource named RecipeDSLSpecNamespace::Bar::BarThingy" do + before(:context) { + + class RecipeDSLSpecNamespace::Bar::BarThingy < BaseThingy + end + + } + + it "bar_thingy does not work" do + expect_converge { + bar_thingy 'blah' do; end + }.to raise_error(NoMethodError) + end + end + + context "with a resource named Chef::Resource::NoNameThingy with resource_name nil" do + before(:context) { + + class Chef::Resource::NoNameThingy < BaseThingy + resource_name nil + end + + } + + it "no_name_thingy does not work" do + expect_converge { + no_name_thingy 'blah' do; end + }.to raise_error(NoMethodError) + end + end + + context "with a resource named AnotherNoNameThingy with resource_name :another_thingy_name" do + before(:context) { + + class AnotherNoNameThingy < BaseThingy + resource_name :another_thingy_name + end + + } + + it "another_no_name_thingy does not work" do + expect_converge { + another_no_name_thingy 'blah' do; end + }.to raise_error(NoMethodError) + end + + it "another_thingy_name works" do + recipe = converge { + another_thingy_name 'blah' do; end + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq(AnotherNoNameThingy) + end + end + + context "with a resource named AnotherNoNameThingy2 with resource_name :another_thingy_name2; resource_name :another_thingy_name3" do + before(:context) { + + class AnotherNoNameThingy2 < BaseThingy + resource_name :another_thingy_name2 + resource_name :another_thingy_name3 + end + + } + + it "another_no_name_thingy does not work" do + expect_converge { + another_no_name_thingy2 'blah' do; end + }.to raise_error(NoMethodError) + end + + it "another_thingy_name2 does not work" do + expect_converge { + another_thingy_name2 'blah' do; end + }.to raise_error(NoMethodError) + end + + it "yet_another_thingy_name3 works" do + recipe = converge { + another_thingy_name3 'blah' do; end + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq(AnotherNoNameThingy2) + end + end + + context "provides overriding resource_name" do + context "with a resource named AnotherNoNameThingy3 with provides :another_no_name_thingy3, os: 'blarghle'" do + before(:context) { + + class AnotherNoNameThingy3 < BaseThingy + resource_name :another_no_name_thingy_3 + provides :another_no_name_thingy3, os: 'blarghle' + end + + } + + it "and os = linux, another_no_name_thingy3 does not work" do + expect_converge { + # TODO this is an ugly way to test, make Cheffish expose node attrs + run_context.node.automatic[:os] = 'linux' + another_no_name_thingy3 'blah' do; end + }.to raise_error(Chef::Exceptions::NoSuchResourceType) + end + + it "and os = blarghle, another_no_name_thingy3 works" do + recipe = converge { + # TODO this is an ugly way to test, make Cheffish expose node attrs + run_context.node.automatic[:os] = 'blarghle' + another_no_name_thingy3 'blah' do; end + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq (AnotherNoNameThingy3) + end + end + + context "with a resource named AnotherNoNameThingy4 with two provides" do + before(:context) { + + class AnotherNoNameThingy4 < BaseThingy + resource_name :another_no_name_thingy_4 + provides :another_no_name_thingy4, os: 'blarghle' + provides :another_no_name_thingy4, platform_family: 'foo' + end + + } + + it "and os = linux, another_no_name_thingy4 does not work" do + expect_converge { + # TODO this is an ugly way to test, make Cheffish expose node attrs + run_context.node.automatic[:os] = 'linux' + another_no_name_thingy4 'blah' do; end + }.to raise_error(Chef::Exceptions::NoSuchResourceType) + end + + it "and os = blarghle, another_no_name_thingy4 works" do + recipe = converge { + # TODO this is an ugly way to test, make Cheffish expose node attrs + run_context.node.automatic[:os] = 'blarghle' + another_no_name_thingy4 'blah' do; end + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq (AnotherNoNameThingy4) + end + + it "and platform_family = foo, another_no_name_thingy4 works" do + recipe = converge { + # TODO this is an ugly way to test, make Cheffish expose node attrs + run_context.node.automatic[:platform_family] = 'foo' + another_no_name_thingy4 'blah' do; end + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq (AnotherNoNameThingy4) + end + end + + context "with a resource named AnotherNoNameThingy5, a different resource_name, and a provides with the original resource_name" do + before(:context) { + + class AnotherNoNameThingy5 < BaseThingy + resource_name :another_thingy_name_for_another_no_name_thingy5 + provides :another_no_name_thingy5, os: 'blarghle' + end + + } + + it "and os = linux, another_no_name_thingy5 does not work" do + expect_converge { + # this is an ugly way to test, make Cheffish expose node attrs + run_context.node.automatic[:os] = 'linux' + another_no_name_thingy5 'blah' do; end + }.to raise_error(Chef::Exceptions::NoSuchResourceType) + end + + it "and os = blarghle, another_no_name_thingy5 works" do + recipe = converge { + # this is an ugly way to test, make Cheffish expose node attrs + run_context.node.automatic[:os] = 'blarghle' + another_no_name_thingy5 'blah' do; end + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq (AnotherNoNameThingy5) + end + + it "the new resource name can be used in a recipe" do + recipe = converge { + another_thingy_name_for_another_no_name_thingy5 'blah' do; end + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq (AnotherNoNameThingy5) + end + end + + context "with a resource named AnotherNoNameThingy6, a provides with the original resource name, and a different resource_name" do + before(:context) { + + class AnotherNoNameThingy6 < BaseThingy + provides :another_no_name_thingy6, os: 'blarghle' + resource_name :another_thingy_name_for_another_no_name_thingy6 + end + + } + + it "and os = linux, another_no_name_thingy6 does not work" do + expect_converge { + # this is an ugly way to test, make Cheffish expose node attrs + run_context.node.automatic[:os] = 'linux' + another_no_name_thingy6 'blah' do; end + }.to raise_error(Chef::Exceptions::NoSuchResourceType) + end + + it "and os = blarghle, another_no_name_thingy6 works" do + recipe = converge { + # this is an ugly way to test, make Cheffish expose node attrs + run_context.node.automatic[:os] = 'blarghle' + another_no_name_thingy6 'blah' do; end + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq (AnotherNoNameThingy6) + end + + it "the new resource name can be used in a recipe" do + recipe = converge { + another_thingy_name_for_another_no_name_thingy6 'blah' do; end + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq (AnotherNoNameThingy6) + end + end + + context "with a resource named AnotherNoNameThingy7, a new resource_name, and provides with that new resource name" do + before(:context) { + + class AnotherNoNameThingy7 < BaseThingy + resource_name :another_thingy_name_for_another_no_name_thingy7 + provides :another_thingy_name_for_another_no_name_thingy7, os: 'blarghle' + end + + } + + it "and os = linux, another_thingy_name_for_another_no_name_thingy7 does not work" do + expect_converge { + # this is an ugly way to test, make Cheffish expose node attrs + run_context.node.automatic[:os] = 'linux' + another_thingy_name_for_another_no_name_thingy7 'blah' do; end + }.to raise_error(Chef::Exceptions::NoSuchResourceType) + end + + it "and os = blarghle, another_thingy_name_for_another_no_name_thingy7 works" do + recipe = converge { + # this is an ugly way to test, make Cheffish expose node attrs + run_context.node.automatic[:os] = 'blarghle' + another_thingy_name_for_another_no_name_thingy7 'blah' do; end + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq (AnotherNoNameThingy7) + end + + it "the old resource name does not work" do + expect_converge { + # this is an ugly way to test, make Cheffish expose node attrs + run_context.node.automatic[:os] = 'linux' + another_no_name_thingy_7 'blah' do; end + }.to raise_error(NoMethodError) + end + end + + # opposite order from the previous test (provides, then resource_name) + context "with a resource named AnotherNoNameThingy8, a provides with a new resource name, and resource_name with that new resource name" do + before(:context) { + + class AnotherNoNameThingy8 < BaseThingy + provides :another_thingy_name_for_another_no_name_thingy8, os: 'blarghle' + resource_name :another_thingy_name_for_another_no_name_thingy8 + end + + } + + it "and os = linux, another_thingy_name_for_another_no_name_thingy8 does not work" do + expect_converge { + # this is an ugly way to test, make Cheffish expose node attrs + run_context.node.automatic[:os] = 'linux' + another_thingy_name_for_another_no_name_thingy8 'blah' do; end + }.to raise_error(Chef::Exceptions::NoSuchResourceType) + end + + it "and os = blarghle, another_thingy_name_for_another_no_name_thingy8 works" do + recipe = converge { + # this is an ugly way to test, make Cheffish expose node attrs + run_context.node.automatic[:os] = 'blarghle' + another_thingy_name_for_another_no_name_thingy8 'blah' do; end + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq (AnotherNoNameThingy8) + end + + it "the old resource name does not work" do + expect_converge { + # this is an ugly way to test, make Cheffish expose node attrs + run_context.node.automatic[:os] = 'linux' + another_thingy_name8 'blah' do; end + }.to raise_error(NoMethodError) + end + end + end + end + + context "provides" do + context "when MySupplier provides :hemlock" do + before(:context) { + + class RecipeDSLSpecNamespace::MySupplier < BaseThingy + resource_name :hemlock + end + + } + + it "my_supplier does not work in a recipe" do + expect_converge { + my_supplier 'blah' do; end + }.to raise_error(NoMethodError) + end + + it "hemlock works in a recipe" do + expect_recipe { + hemlock 'blah' do; end + }.to emit_no_warnings_or_errors + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::MySupplier + end + end + + context "when Thingy3 has resource_name :thingy3" do + before(:context) { + + class RecipeDSLSpecNamespace::Thingy3 < BaseThingy + resource_name :thingy3 + end + + } + + it "thingy3 works in a recipe" do + expect_recipe { + thingy3 'blah' do; end + }.to emit_no_warnings_or_errors + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy3 + end + + context "and Thingy4 has resource_name :thingy3" do + before(:context) { + + class RecipeDSLSpecNamespace::Thingy4 < BaseThingy + resource_name :thingy3 + end + + } + + it "thingy3 works in a recipe and yields Thingy3 (the alphabetical one)" do + recipe = converge { + thingy3 'blah' do; end + } + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy3 + end + + it "thingy4 does not work in a recipe" do + expect_converge { + thingy4 'blah' do; end + }.to raise_error(NoMethodError) + end + + it "resource_matching_short_name returns Thingy4" do + expect(Chef::Resource.resource_matching_short_name(:thingy3)).to eq RecipeDSLSpecNamespace::Thingy3 + end + end + end + + context "when Thingy5 has resource_name :thingy5 and provides :thingy5reverse, :thingy5_2 and :thingy5_2reverse" do + before(:context) { + + class RecipeDSLSpecNamespace::Thingy5 < BaseThingy + resource_name :thingy5 + provides :thingy5reverse + provides :thingy5_2 + provides :thingy5_2reverse + end + + } + + it "thingy5 works in a recipe" do + expect_recipe { + thingy5 'blah' do; end + }.to emit_no_warnings_or_errors + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy5 + end + + context "and Thingy6 provides :thingy5" do + before(:context) { + + class RecipeDSLSpecNamespace::Thingy6 < BaseThingy + resource_name :thingy6 + provides :thingy5 + end + + } + + it "thingy6 works in a recipe and yields Thingy6" do + recipe = converge { + thingy6 'blah' do; end + } + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy6 + end + + it "thingy5 works in a recipe and yields Foo::Thingy5 (the alphabetical one)" do + recipe = converge { + thingy5 'blah' do; end + } + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy5 + end + + it "resource_matching_short_name returns Thingy5" do + expect(Chef::Resource.resource_matching_short_name(:thingy5)).to eq RecipeDSLSpecNamespace::Thingy5 + end + + context "and AThingy5 provides :thingy5reverse" do + before(:context) { + + class RecipeDSLSpecNamespace::AThingy5 < BaseThingy + resource_name :thingy5reverse + end + + } + + it "thingy5reverse works in a recipe and yields AThingy5 (the alphabetical one)" do + recipe = converge { + thingy5reverse 'blah' do; end + } + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::AThingy5 + end + end + + context "and ZRecipeDSLSpecNamespace::Thingy5 provides :thingy5_2" do + before(:context) { + + module ZRecipeDSLSpecNamespace + class Thingy5 < BaseThingy + resource_name :thingy5_2 + end + end + + } + + it "thingy5_2 works in a recipe and yields the RecipeDSLSpaceNamespace one (the alphabetical one)" do + recipe = converge { + thingy5_2 'blah' do; end + } + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy5 + end + end + + context "and ARecipeDSLSpecNamespace::Thingy5 provides :thingy5_2" do + before(:context) { + + module ARecipeDSLSpecNamespace + class Thingy5 < BaseThingy + resource_name :thingy5_2reverse + end + end + + } + + it "thingy5_2reverse works in a recipe and yields the ARecipeDSLSpaceNamespace one (the alphabetical one)" do + recipe = converge { + thingy5_2reverse 'blah' do; end + } + expect(BaseThingy.created_resource).to eq ARecipeDSLSpecNamespace::Thingy5 + end + end + end + + context "when Thingy3 has resource_name :thingy3" do + before(:context) { + + class RecipeDSLSpecNamespace::Thingy3 < BaseThingy + resource_name :thingy3 + end + + } + + it "thingy3 works in a recipe" do + expect_recipe { + thingy3 'blah' do; end + }.to emit_no_warnings_or_errors + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy3 + end + + context "and Thingy4 has resource_name :thingy3" do + before(:context) { + + class RecipeDSLSpecNamespace::Thingy4 < BaseThingy + resource_name :thingy3 + end + + } + + it "thingy3 works in a recipe and yields Thingy3 (the alphabetical one)" do + recipe = converge { + thingy3 'blah' do; end + } + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy3 + end + + it "thingy4 does not work in a recipe" do + expect_converge { + thingy4 'blah' do; end + }.to raise_error(NoMethodError) + end + + it "resource_matching_short_name returns Thingy4" do + expect(Chef::Resource.resource_matching_short_name(:thingy3)).to eq RecipeDSLSpecNamespace::Thingy3 + end + end + + context "and Thingy4 has resource_name :thingy3" do + before(:context) { + + class RecipeDSLSpecNamespace::Thingy4 < BaseThingy + resource_name :thingy3 + end + + } + + it "thingy3 works in a recipe and yields Thingy3 (the alphabetical one)" do + recipe = converge { + thingy3 'blah' do; end + } + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy3 + end + + it "thingy4 does not work in a recipe" do + expect_converge { + thingy4 'blah' do; end + }.to raise_error(NoMethodError) + end + + it "resource_matching_short_name returns Thingy4" do + expect(Chef::Resource.resource_matching_short_name(:thingy3)).to eq RecipeDSLSpecNamespace::Thingy3 + end + end + end + + end + + context "when Thingy7 provides :thingy8" do + before(:context) { + + class RecipeDSLSpecNamespace::Thingy7 < BaseThingy + resource_name :thingy7 + provides :thingy8 + end + + } + + context "and Thingy8 has resource_name :thingy8" do + before(:context) { + + class RecipeDSLSpecNamespace::Thingy8 < BaseThingy + resource_name :thingy8 + end + + } + + it "thingy7 works in a recipe and yields Thingy7" do + recipe = converge { + thingy7 'blah' do; end + } + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy7 + end + + it "thingy8 works in a recipe and yields Thingy7 (alphabetical)" do + recipe = converge { + thingy8 'blah' do; end + } + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy7 + end + + it "resource_matching_short_name returns Thingy8" do + expect(Chef::Resource.resource_matching_short_name(:thingy8)).to eq RecipeDSLSpecNamespace::Thingy8 + end + end + end + + context "when Thingy12 provides :thingy12, :twizzle and :twizzle2" do + before(:context) { + + class RecipeDSLSpecNamespace::Thingy12 < BaseThingy + resource_name :thingy12 + provides :twizzle + provides :twizzle2 + end + + } + + it "thingy12 works in a recipe and yields Thingy12" do + expect_recipe { + thingy12 'blah' do; end + }.to emit_no_warnings_or_errors + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy12 + end + + it "twizzle works in a recipe and yields Thingy12" do + expect_recipe { + twizzle 'blah' do; end + }.to emit_no_warnings_or_errors + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy12 + end + + it "twizzle2 works in a recipe and yields Thingy12" do + expect_recipe { + twizzle2 'blah' do; end + }.to emit_no_warnings_or_errors + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy12 + end + end + + context "with platform-specific resources 'my_super_thingy_foo' and 'my_super_thingy_bar'" do + before(:context) { + class MySuperThingyFoo < BaseThingy + resource_name :my_super_thingy_foo + provides :my_super_thingy, platform: 'foo' + end + + class MySuperThingyBar < BaseThingy + resource_name :my_super_thingy_bar + provides :my_super_thingy, platform: 'bar' + end + } + + it "A run with platform 'foo' uses MySuperThingyFoo" do + r = Cheffish::ChefRun.new(chef_config) + r.client.run_context.node.automatic['platform'] = 'foo' + r.compile_recipe { + my_super_thingy 'blah' do; end + } + r.converge + expect(r).to emit_no_warnings_or_errors + expect(BaseThingy.created_resource).to eq MySuperThingyFoo + end + + it "A run with platform 'bar' uses MySuperThingyBar" do + r = Cheffish::ChefRun.new(chef_config) + r.client.run_context.node.automatic['platform'] = 'bar' + r.compile_recipe { + my_super_thingy 'blah' do; end + } + r.converge + expect(r).to emit_no_warnings_or_errors + expect(BaseThingy.created_resource).to eq MySuperThingyBar + end + + it "A run with platform 'x' reports that my_super_thingy is not supported" do + r = Cheffish::ChefRun.new(chef_config) + r.client.run_context.node.automatic['platform'] = 'x' + expect { + r.compile_recipe { + my_super_thingy 'blah' do; end + } + }.to raise_error(Chef::Exceptions::NoSuchResourceType) + end + end + + context "when Thingy10 provides :thingy10" do + before(:context) { + class RecipeDSLSpecNamespace::Thingy10 < BaseThingy + resource_name :thingy10 + end + } + + it "declaring a resource providing the same :thingy10 with override: true does not produce a warning" do + expect(Chef::Log).not_to receive(:warn) + class RecipeDSLSpecNamespace::Thingy10AlternateProvider < BaseThingy + provides :thingy10, override: true + end + end + end + + context "when Thingy11 provides :thingy11" do + before(:context) { + class RecipeDSLSpecNamespace::Thingy11 < BaseThingy + resource_name :thingy10 + end + } + + it "declaring a resource providing the same :thingy11 with os: 'linux' does not produce a warning" do + expect(Chef::Log).not_to receive(:warn) + class RecipeDSLSpecNamespace::Thingy11AlternateProvider < BaseThingy + provides :thingy11, os: 'linux' + end + end + end + end + + context "with a resource named 'B' with resource name :two_classes_one_dsl" do + let(:two_classes_one_dsl) { :"two_classes_one_dsl#{Namer.current_index}" } + let(:resource_class) { + result = Class.new(BaseThingy) do + def self.name + "B" + end + def self.to_s; name; end + def self.inspect; name.inspect; end + end + result.resource_name two_classes_one_dsl + result + } + before { resource_class } # pull on it so it gets defined before the recipe runs + + context "and another resource named 'A' with resource_name :two_classes_one_dsl" do + let(:resource_class_a) { + result = Class.new(BaseThingy) do + def self.name + "A" + end + def self.to_s; name; end + def self.inspect; name.inspect; end + end + result.resource_name two_classes_one_dsl + result + } + before { resource_class_a } # pull on it so it gets defined before the recipe runs + + it "two_classes_one_dsl resolves to A (alphabetically earliest)" do + two_classes_one_dsl = self.two_classes_one_dsl + recipe = converge { + instance_eval("#{two_classes_one_dsl} 'blah'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq resource_class_a + end + + it "resource_matching_short_name returns B" do + expect(Chef::Resource.resource_matching_short_name(two_classes_one_dsl)).to eq resource_class_a + end + end + + context "and another resource named 'Z' with resource_name :two_classes_one_dsl" do + let(:resource_class_z) { + result = Class.new(BaseThingy) do + def self.name + "Z" + end + def self.to_s; name; end + def self.inspect; name.inspect; end + end + result.resource_name two_classes_one_dsl + result + } + before { resource_class_z } # pull on it so it gets defined before the recipe runs + + it "two_classes_one_dsl resolves to B (alphabetically earliest)" do + two_classes_one_dsl = self.two_classes_one_dsl + recipe = converge { + instance_eval("#{two_classes_one_dsl} 'blah'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq resource_class + end + + it "resource_matching_short_name returns B" do + expect(Chef::Resource.resource_matching_short_name(two_classes_one_dsl)).to eq resource_class + end + + context "and a priority array [ Z, B ]" do + before do + Chef.set_resource_priority_array(two_classes_one_dsl, [ resource_class_z, resource_class ]) + end + + it "two_classes_one_dsl resolves to Z (respects the priority array)" do + two_classes_one_dsl = self.two_classes_one_dsl + recipe = converge { + instance_eval("#{two_classes_one_dsl} 'blah'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq resource_class_z + end + + it "resource_matching_short_name returns B" do + expect(Chef::Resource.resource_matching_short_name(two_classes_one_dsl)).to eq resource_class + end + + context "when Z provides(:two_classes_one_dsl) { false }" do + before do + resource_class_z.provides(two_classes_one_dsl) { false } + end + + it "two_classes_one_dsl resolves to B (picks the next thing in the priority array)" do + two_classes_one_dsl = self.two_classes_one_dsl + recipe = converge { + instance_eval("#{two_classes_one_dsl} 'blah'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq resource_class + end + + it "resource_matching_short_name returns B" do + expect(Chef::Resource.resource_matching_short_name(two_classes_one_dsl)).to eq resource_class + end + end + end + + context "and priority arrays [ B ] and [ Z ]" do + before do + Chef.set_resource_priority_array(two_classes_one_dsl, [ resource_class ]) + Chef.set_resource_priority_array(two_classes_one_dsl, [ resource_class_z ]) + end + + it "two_classes_one_dsl resolves to Z (respects the most recent priority array)" do + two_classes_one_dsl = self.two_classes_one_dsl + recipe = converge { + instance_eval("#{two_classes_one_dsl} 'blah'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq resource_class_z + end + + it "resource_matching_short_name returns B" do + expect(Chef::Resource.resource_matching_short_name(two_classes_one_dsl)).to eq resource_class + end + + context "when Z provides(:two_classes_one_dsl) { false }" do + before do + resource_class_z.provides(two_classes_one_dsl) { false } + end + + it "two_classes_one_dsl resolves to B (picks the first match from the other priority array)" do + two_classes_one_dsl = self.two_classes_one_dsl + recipe = converge { + instance_eval("#{two_classes_one_dsl} 'blah'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq resource_class + end + + it "resource_matching_short_name returns B" do + expect(Chef::Resource.resource_matching_short_name(two_classes_one_dsl)).to eq resource_class + end + end + end + + context "and a priority array [ Z ]" do + before do + Chef.set_resource_priority_array(two_classes_one_dsl, [ resource_class_z ]) + end + + context "when Z provides(:two_classes_one_dsl) { false }" do + before do + resource_class_z.provides(two_classes_one_dsl) { false } + end + + it "two_classes_one_dsl resolves to B (picks the first match outside the priority array)" do + two_classes_one_dsl = self.two_classes_one_dsl + recipe = converge { + instance_eval("#{two_classes_one_dsl} 'blah'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq resource_class + end + + it "resource_matching_short_name returns B" do + expect(Chef::Resource.resource_matching_short_name(two_classes_one_dsl)).to eq resource_class + end + end + end + + end + + context "and a provider named 'B' which provides :two_classes_one_dsl" do + before do + resource_class.send(:define_method, :provider) { nil } + end + + let(:provider_class) { + result = Class.new(BaseThingy::Provider) do + def self.name + "B" + end + def self.to_s; name; end + def self.inspect; name.inspect; end + end + result.provides two_classes_one_dsl + result + } + before { provider_class } # pull on it so it gets defined before the recipe runs + + context "and another provider named 'A'" do + let(:provider_class_a) { + result = Class.new(BaseThingy::Provider) do + def self.name + "A" + end + def self.to_s; name; end + def self.inspect; name.inspect; end + end + result + } + context "which provides :two_classes_one_dsl" do + before { provider_class_a.provides two_classes_one_dsl } + + it "two_classes_one_dsl resolves to A (alphabetically earliest)" do + two_classes_one_dsl = self.two_classes_one_dsl + recipe = converge { + instance_eval("#{two_classes_one_dsl} 'blah'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_provider).to eq provider_class_a + end + end + context "which provides(:two_classes_one_dsl) { false }" do + before { provider_class_a.provides(two_classes_one_dsl) { false } } + + it "two_classes_one_dsl resolves to B (since A declined)" do + two_classes_one_dsl = self.two_classes_one_dsl + recipe = converge { + instance_eval("#{two_classes_one_dsl} 'blah'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_provider).to eq provider_class + end + end + end + + context "and another provider named 'Z'" do + let(:provider_class_z) { + result = Class.new(BaseThingy::Provider) do + def self.name + "Z" + end + def self.to_s; name; end + def self.inspect; name.inspect; end + end + result + } + before { provider_class_z } # pull on it so it gets defined before the recipe runs + + context "which provides :two_classes_one_dsl" do + before { provider_class_z.provides two_classes_one_dsl } + + it "two_classes_one_dsl resolves to B (alphabetically earliest)" do + two_classes_one_dsl = self.two_classes_one_dsl + recipe = converge { + instance_eval("#{two_classes_one_dsl} 'blah'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_provider).to eq provider_class + end + + context "with a priority array [ Z, B ]" do + before { Chef.set_provider_priority_array two_classes_one_dsl, [ provider_class_z, provider_class ] } + + it "two_classes_one_dsl resolves to Z (respects the priority map)" do + two_classes_one_dsl = self.two_classes_one_dsl + recipe = converge { + instance_eval("#{two_classes_one_dsl} 'blah'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_provider).to eq provider_class_z + end + end + end + + context "which provides(:two_classes_one_dsl) { false }" do + before { provider_class_z.provides(two_classes_one_dsl) { false } } + + context "with a priority array [ Z, B ]" do + before { Chef.set_provider_priority_array two_classes_one_dsl, [ provider_class_z, provider_class ] } + + it "two_classes_one_dsl resolves to B (the next one in the priority map)" do + two_classes_one_dsl = self.two_classes_one_dsl + recipe = converge { + instance_eval("#{two_classes_one_dsl} 'blah'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_provider).to eq provider_class + end + end + + context "with priority arrays [ B ] and [ Z ]" do + before { Chef.set_provider_priority_array two_classes_one_dsl, [ provider_class_z ] } + before { Chef.set_provider_priority_array two_classes_one_dsl, [ provider_class ] } + + it "two_classes_one_dsl resolves to B (the one in the next priority map)" do + two_classes_one_dsl = self.two_classes_one_dsl + recipe = converge { + instance_eval("#{two_classes_one_dsl} 'blah'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_provider).to eq provider_class + end + end + end + end + end + + context "and another resource Blarghle with provides :two_classes_one_dsl, os: 'blarghle'" do + let(:resource_class_blarghle) { + result = Class.new(BaseThingy) do + def self.name + "Blarghle" + end + def self.to_s; name; end + def self.inspect; name.inspect; end + end + result.resource_name two_classes_one_dsl + result.provides two_classes_one_dsl, os: 'blarghle' + result + } + before { resource_class_blarghle } # pull on it so it gets defined before the recipe runs + + it "on os = blarghle, two_classes_one_dsl resolves to Blarghle" do + two_classes_one_dsl = self.two_classes_one_dsl + recipe = converge { + # this is an ugly way to test, make Cheffish expose node attrs + run_context.node.automatic[:os] = 'blarghle' + instance_eval("#{two_classes_one_dsl} 'blah' do; end") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq resource_class_blarghle + end + + it "on os = linux, two_classes_one_dsl resolves to B" do + two_classes_one_dsl = self.two_classes_one_dsl + recipe = converge { + # this is an ugly way to test, make Cheffish expose node attrs + run_context.node.automatic[:os] = 'linux' + instance_eval("#{two_classes_one_dsl} 'blah' do; end") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq resource_class + end + end + end + + context "with a resource MyResource" do + let(:resource_class) { Class.new(BaseThingy) do + def self.called_provides + @called_provides + end + def to_s + "MyResource" + end + end } + let(:my_resource) { :"my_resource#{Namer.current_index}" } + let(:blarghle_blarghle_little_star) { :"blarghle_blarghle_little_star#{Namer.current_index}" } + + context "with resource_name :my_resource" do + before { + resource_class.resource_name my_resource + } + + context "with provides? returning true to my_resource" do + before { + my_resource = self.my_resource + resource_class.define_singleton_method(:provides?) do |node, resource_name| + @called_provides = true + resource_name == my_resource + end + } + + it "my_resource returns the resource and calls provides?, but does not emit a warning" do + dsl_name = self.my_resource + recipe = converge { + instance_eval("#{dsl_name} 'foo'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq resource_class + expect(resource_class.called_provides).to be_truthy + end + end + + context "with provides? returning true to blarghle_blarghle_little_star and not resource_name" do + before do + blarghle_blarghle_little_star = self.blarghle_blarghle_little_star + resource_class.define_singleton_method(:provides?) do |node, resource_name| + @called_provides = true + resource_name == blarghle_blarghle_little_star + end + end + + it "my_resource does not return the resource" do + dsl_name = self.my_resource + expect_converge { + instance_eval("#{dsl_name} 'foo'") + }.to raise_error(Chef::Exceptions::NoSuchResourceType) + expect(resource_class.called_provides).to be_truthy + end + + it "blarghle_blarghle_little_star 'foo' returns the resource and emits a warning" do + Chef::Config[:treat_deprecation_warnings_as_errors] = false + dsl_name = self.blarghle_blarghle_little_star + recipe = converge { + instance_eval("#{dsl_name} 'foo'") + } + expect(recipe.logged_warnings).to include "WARN: #{resource_class}.provides? returned true when asked if it provides DSL #{dsl_name}, but provides :#{dsl_name} was never called!" + expect(BaseThingy.created_resource).to eq resource_class + expect(resource_class.called_provides).to be_truthy + end + end + + context "and a provider" do + let(:provider_class) do + Class.new(BaseThingy::Provider) do + def self.name + "MyProvider" + end + def self.to_s; name; end + def self.inspect; name.inspect; end + def self.called_provides + @called_provides + end + end + end + + before do + resource_class.send(:define_method, :provider) { nil } + end + + context "that provides :my_resource" do + before do + provider_class.provides my_resource + end + + context "with supports? returning true" do + before do + provider_class.define_singleton_method(:supports?) { |resource,action| true } + end + + it "my_resource runs the provider and does not emit a warning" do + my_resource = self.my_resource + recipe = converge { + instance_eval("#{my_resource} 'foo'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_provider).to eq provider_class + end + + context "and another provider supporting :my_resource with supports? false" do + let(:provider_class2) do + Class.new(BaseThingy::Provider) do + def self.name + "MyProvider2" + end + def self.to_s; name; end + def self.inspect; name.inspect; end + def self.called_provides + @called_provides + end + provides my_resource + def self.supports?(resource, action) + false + end + end + end + + it "my_resource runs the first provider" do + my_resource = self.my_resource + recipe = converge { + instance_eval("#{my_resource} 'foo'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_provider).to eq provider_class + end + end + end + + context "with supports? returning false" do + before do + provider_class.define_singleton_method(:supports?) { |resource,action| false } + end + + # TODO no warning? ick + it "my_resource runs the provider anyway" do + my_resource = self.my_resource + recipe = converge { + instance_eval("#{my_resource} 'foo'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_provider).to eq provider_class + end + + context "and another provider supporting :my_resource with supports? true" do + let(:provider_class2) do + my_resource = self.my_resource + Class.new(BaseThingy::Provider) do + def self.name + "MyProvider2" + end + def self.to_s; name; end + def self.inspect; name.inspect; end + def self.called_provides + @called_provides + end + provides my_resource + def self.supports?(resource, action) + true + end + end + end + before { provider_class2 } # make sure the provider class shows up + + it "my_resource runs the other provider" do + my_resource = self.my_resource + recipe = converge { + instance_eval("#{my_resource} 'foo'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_provider).to eq provider_class2 + end + end + end + end + + context "with provides? returning true" do + before { + my_resource = self.my_resource + provider_class.define_singleton_method(:provides?) do |node, resource| + @called_provides = true + resource.declared_type == my_resource + end + } + + context "that provides :my_resource" do + before { + provider_class.provides my_resource + } + + it "my_resource calls the provider (and calls provides?), but does not emit a warning" do + my_resource = self.my_resource + recipe = converge { + instance_eval("#{my_resource} 'foo'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_provider).to eq provider_class + expect(provider_class.called_provides).to be_truthy + end + end + + context "that does not call provides :my_resource" do + it "my_resource calls the provider (and calls provides?), and emits a warning" do + Chef::Config[:treat_deprecation_warnings_as_errors] = false + my_resource = self.my_resource + recipe = converge { + instance_eval("#{my_resource} 'foo'") + } + expect(recipe.logged_warnings).to include("WARN: #{provider_class}.provides? returned true when asked if it provides DSL #{my_resource}, but provides :#{my_resource} was never called!") + expect(BaseThingy.created_provider).to eq provider_class + expect(provider_class.called_provides).to be_truthy + end + end + end + + context "with provides? returning false to my_resource" do + before { + my_resource = self.my_resource + provider_class.define_singleton_method(:provides?) do |node, resource| + @called_provides = true + false + end + } + + context "that provides :my_resource" do + before { + provider_class.provides my_resource + } + + it "my_resource fails to find a provider (and calls provides)" do + my_resource = self.my_resource + expect_converge { + instance_eval("#{my_resource} 'foo'") + }.to raise_error(Chef::Exceptions::ProviderNotFound) + expect(provider_class.called_provides).to be_truthy + end + end + + context "that does not provide :my_resource" do + it "my_resource fails to find a provider (and calls provides)" do + my_resource = self.my_resource + expect_converge { + instance_eval("#{my_resource} 'foo'") + }.to raise_error(Chef::Exceptions::ProviderNotFound) + expect(provider_class.called_provides).to be_truthy + end + end + end + end + end + end + end + + before(:all) { Namer.current_index = 0 } + before { Namer.current_index += 1 } + + context "with an LWRP that declares actions" do + let(:resource_class) { + Class.new(Chef::Resource::LWRPBase) do + provides :"recipe_dsl_spec#{Namer.current_index}" + actions :create + end + } + let(:resource) { + resource_class.new("blah", run_context) + } + it "The actions are part of actions along with :nothing" do + expect(resource_class.actions).to eq [ :nothing, :create ] + end + it "The actions are part of allowed_actions along with :nothing" do + expect(resource.allowed_actions).to eq [ :nothing, :create ] + end + + context "and a subclass that declares more actions" do + let(:subresource_class) { + Class.new(Chef::Resource::LWRPBase) do + provides :"recipe_dsl_spec_sub#{Namer.current_index}" + actions :delete + end + } + let(:subresource) { + subresource_class.new("subblah", run_context) + } + + it "The parent class actions are not part of actions" do + expect(subresource_class.actions).to eq [ :nothing, :delete ] + end + it "The parent class actions are not part of allowed_actions" do + expect(subresource.allowed_actions).to eq [ :nothing, :delete ] + end + it "The parent class actions do not change" do + expect(resource_class.actions).to eq [ :nothing, :create ] + expect(resource.allowed_actions).to eq [ :nothing, :create ] + end + end + end + + context "with a dynamically defined resource and regular provider" do + before(:context) do + Class.new(Chef::Resource) do + resource_name :lw_resource_with_hw_provider_test_case + default_action :create + attr_accessor :created_provider + end + class Chef::Provider::LwResourceWithHwProviderTestCase < Chef::Provider + def load_current_resource + end + def action_create + new_resource.created_provider = self.class + end + end + end + + it "looks up the provider in Chef::Provider converting the resource name from snake case to camel case" do + resource = nil + recipe = converge { + resource = lw_resource_with_hw_provider_test_case 'blah' do; end + } + expect(resource.created_provider).to eq(Chef::Provider::LwResourceWithHwProviderTestCase) + end + end +end diff --git a/spec/integration/recipes/resource_action_spec.rb b/spec/integration/recipes/resource_action_spec.rb new file mode 100644 index 0000000000..53611c144f --- /dev/null +++ b/spec/integration/recipes/resource_action_spec.rb @@ -0,0 +1,356 @@ +require 'support/shared/integration/integration_helper' + +describe "Resource.action" do + include IntegrationSupport + + shared_context "ActionJackson" do + it "The default action is the first declared action" do + converge <<-EOM, __FILE__, __LINE__+1 + #{resource_dsl} 'hi' do + foo 'foo!' + end + EOM + expect(ActionJackson.ran_action).to eq :access_recipe_dsl + expect(ActionJackson.succeeded).to eq true + end + + it "The action can access recipe DSL" do + converge <<-EOM, __FILE__, __LINE__+1 + #{resource_dsl} 'hi' do + foo 'foo!' + action :access_recipe_dsl + end + EOM + expect(ActionJackson.ran_action).to eq :access_recipe_dsl + expect(ActionJackson.succeeded).to eq true + end + + it "The action can access attributes" do + converge <<-EOM, __FILE__, __LINE__+1 + #{resource_dsl} 'hi' do + foo 'foo!' + action :access_attribute + end + EOM + expect(ActionJackson.ran_action).to eq :access_attribute + expect(ActionJackson.succeeded).to eq 'foo!' + end + + it "The action can access public methods" do + converge <<-EOM, __FILE__, __LINE__+1 + #{resource_dsl} 'hi' do + foo 'foo!' + action :access_method + end + EOM + expect(ActionJackson.ran_action).to eq :access_method + expect(ActionJackson.succeeded).to eq 'foo_public!' + end + + it "The action can access protected methods" do + converge <<-EOM, __FILE__, __LINE__+1 + #{resource_dsl} 'hi' do + foo 'foo!' + action :access_protected_method + end + EOM + expect(ActionJackson.ran_action).to eq :access_protected_method + expect(ActionJackson.succeeded).to eq 'foo_protected!' + end + + it "The action cannot access private methods" do + expect { + converge(<<-EOM, __FILE__, __LINE__+1) + #{resource_dsl} 'hi' do + foo 'foo!' + action :access_private_method + end + EOM + }.to raise_error(NameError) + expect(ActionJackson.ran_action).to eq :access_private_method + end + + it "The action cannot access resource instance variables" do + converge <<-EOM, __FILE__, __LINE__+1 + #{resource_dsl} 'hi' do + foo 'foo!' + action :access_instance_variable + end + EOM + expect(ActionJackson.ran_action).to eq :access_instance_variable + expect(ActionJackson.succeeded).to be_nil + end + + it "The action does not compile until the prior resource has converged" do + converge <<-EOM, __FILE__, __LINE__+1 + ruby_block 'wow' do + block do + ActionJackson.ruby_block_converged = 'ruby_block_converged!' + end + end + + #{resource_dsl} 'hi' do + foo 'foo!' + action :access_class_method + end + EOM + expect(ActionJackson.ran_action).to eq :access_class_method + expect(ActionJackson.succeeded).to eq 'ruby_block_converged!' + end + + it "The action's resources converge before the next resource converges" do + converge <<-EOM, __FILE__, __LINE__+1 + #{resource_dsl} 'hi' do + foo 'foo!' + action :access_attribute + end + + ruby_block 'wow' do + block do + ActionJackson.ruby_block_converged = ActionJackson.succeeded + end + end + EOM + expect(ActionJackson.ran_action).to eq :access_attribute + expect(ActionJackson.succeeded).to eq 'foo!' + expect(ActionJackson.ruby_block_converged).to eq 'foo!' + end + end + + context "With resource 'action_jackson'" do + before(:context) { + class ActionJackson < Chef::Resource + use_automatic_resource_name + def foo(value=nil) + @foo = value if value + @foo + end + def blarghle(value=nil) + @blarghle = value if value + @blarghle + end + + class <<self + attr_accessor :ran_action + attr_accessor :succeeded + attr_accessor :ruby_block_converged + end + + public + def foo_public + 'foo_public!' + end + protected + def foo_protected + 'foo_protected!' + end + private + def foo_private + 'foo_private!' + end + + public + action :access_recipe_dsl do + ActionJackson.ran_action = :access_recipe_dsl + ruby_block 'hi there' do + block do + ActionJackson.succeeded = true + end + end + end + action :access_attribute do + ActionJackson.ran_action = :access_attribute + ActionJackson.succeeded = foo + ActionJackson.succeeded += " #{blarghle}" if blarghle + ActionJackson.succeeded += " #{bar}" if respond_to?(:bar) + end + action :access_attribute2 do + ActionJackson.ran_action = :access_attribute2 + ActionJackson.succeeded = foo + ActionJackson.succeeded += " #{blarghle}" if blarghle + ActionJackson.succeeded += " #{bar}" if respond_to?(:bar) + end + action :access_method do + ActionJackson.ran_action = :access_method + ActionJackson.succeeded = foo_public + end + action :access_protected_method do + ActionJackson.ran_action = :access_protected_method + ActionJackson.succeeded = foo_protected + end + action :access_private_method do + ActionJackson.ran_action = :access_private_method + ActionJackson.succeeded = foo_private + end + action :access_instance_variable do + ActionJackson.ran_action = :access_instance_variable + ActionJackson.succeeded = @foo + end + action :access_class_method do + ActionJackson.ran_action = :access_class_method + ActionJackson.succeeded = ActionJackson.ruby_block_converged + end + end + } + before(:each) { + ActionJackson.ran_action = :error + ActionJackson.succeeded = :error + ActionJackson.ruby_block_converged = :error + } + + it_behaves_like "ActionJackson" do + let(:resource_dsl) { :action_jackson } + end + + context "And 'action_jackgrandson' inheriting from ActionJackson and changing nothing" do + before(:context) { + class ActionJackgrandson < ActionJackson + use_automatic_resource_name + end + } + + it_behaves_like "ActionJackson" do + let(:resource_dsl) { :action_jackgrandson } + end + end + + context "And 'action_jackalope' inheriting from ActionJackson with an extra attribute and action" do + before(:context) { + class ActionJackalope < ActionJackson + use_automatic_resource_name + + def foo(value=nil) + @foo = "#{value}alope" if value + @foo + end + def bar(value=nil) + @bar = "#{value}alope" if value + @bar + end + class <<self + attr_accessor :jackalope_ran + end + action :access_jackalope do + ActionJackalope.jackalope_ran = :access_jackalope + ActionJackalope.succeeded = "#{foo} #{blarghle} #{bar}" + end + action :access_attribute do + super() + ActionJackalope.jackalope_ran = :access_attribute + ActionJackalope.succeeded = ActionJackson.succeeded + end + end + } + before do + ActionJackalope.jackalope_ran = nil + end + + context "action_jackson still behaves the same" do + it_behaves_like "ActionJackson" do + let(:resource_dsl) { :action_jackson } + end + end + + it "The default action remains the same even though new actions were specified first" do + converge { + action_jackalope 'hi' do + foo 'foo!' + bar 'bar!' + end + } + expect(ActionJackson.ran_action).to eq :access_recipe_dsl + expect(ActionJackson.succeeded).to eq true + end + + it "new actions run, and can access overridden, new, and overridden attributes" do + converge { + action_jackalope 'hi' do + foo 'foo!' + bar 'bar!' + blarghle 'blarghle!' + action :access_jackalope + end + } + expect(ActionJackalope.jackalope_ran).to eq :access_jackalope + expect(ActionJackalope.succeeded).to eq "foo!alope blarghle! bar!alope" + end + + it "overridden actions run, call super, and can access overridden, new, and overridden attributes" do + converge { + action_jackalope 'hi' do + foo 'foo!' + bar 'bar!' + blarghle 'blarghle!' + action :access_attribute + end + } + expect(ActionJackson.ran_action).to eq :access_attribute + expect(ActionJackson.succeeded).to eq "foo!alope blarghle! bar!alope" + expect(ActionJackalope.jackalope_ran).to eq :access_attribute + expect(ActionJackalope.succeeded).to eq "foo!alope blarghle! bar!alope" + end + + it "non-overridden actions run and can access overridden and non-overridden variables (but not necessarily new ones)" do + converge { + action_jackalope 'hi' do + foo 'foo!' + bar 'bar!' + blarghle 'blarghle!' + action :access_attribute2 + end + } + expect(ActionJackson.ran_action).to eq :access_attribute2 + expect(ActionJackson.succeeded).to eq("foo!alope blarghle! bar!alope").or(eq("foo!alope blarghle!")) + end + end + end + + context "With a resource with no actions" do + before(:context) { + class NoActionJackson < Chef::Resource + use_automatic_resource_name + + def foo(value=nil) + @foo = value if value + @foo + end + + class <<self + attr_accessor :action_was + end + end + } + it "The default action is :nothing" do + converge { + no_action_jackson 'hi' do + foo 'foo!' + NoActionJackson.action_was = action + end + } + expect(NoActionJackson.action_was).to eq [:nothing] + end + end + + context "With a resource with action a-b-c d" do + before(:context) { + class WeirdActionJackson < Chef::Resource + use_automatic_resource_name + + class <<self + attr_accessor :action_was + end + + action "a-b-c d" do + WeirdActionJackson.action_was = action + end + end + } + + it "Running the action works" do + expect_recipe { + weird_action_jackson 'hi' + }.to be_up_to_date + expect(WeirdActionJackson.action_was).to eq :"a-b-c d" + end + end +end diff --git a/spec/integration/recipes/resource_converge_if_changed_spec.rb b/spec/integration/recipes/resource_converge_if_changed_spec.rb new file mode 100644 index 0000000000..d00252a717 --- /dev/null +++ b/spec/integration/recipes/resource_converge_if_changed_spec.rb @@ -0,0 +1,423 @@ +require 'support/shared/integration/integration_helper' + +describe "Resource::ActionProvider#converge_if_changed" do + include IntegrationSupport + + module Namer + extend self + attr_accessor :current_index + def incrementing_value + @incrementing_value += 1 + @incrementing_value + end + attr_writer :incrementing_value + end + + before(:all) { Namer.current_index = 1 } + before { Namer.current_index += 1 } + before { Namer.incrementing_value = 0 } + + context "when the resource has identity, state and control properties" do + let(:resource_name) { :"converge_if_changed_dsl#{Namer.current_index}" } + let(:resource_class) { + result = Class.new(Chef::Resource) do + def self.to_s; resource_name; end + def self.inspect; resource_name.inspect; end + property :identity1, identity: true, default: 'default_identity1' + property :control1, desired_state: false, default: 'default_control1' + property :state1, default: 'default_state1' + property :state2, default: 'default_state2' + attr_accessor :converged + def initialize(*args) + super + @converged = 0 + end + end + result.resource_name resource_name + result + } + let(:converged_recipe) { converge(converge_recipe) } + let(:resource) { converged_recipe.resources.first } + + context "and converge_if_changed with no parameters" do + before :each do + resource_class.action :create do + converge_if_changed do + new_resource.converged += 1 + end + end + end + + context "and current_resource with state1=current, state2=current" do + before :each do + resource_class.load_current_value do + state1 'current_state1' + state2 'current_state2' + end + end + + context "and nothing is set" do + let(:converge_recipe) { "#{resource_name} 'blah'" } + + it "the resource updates nothing" do + expect(resource.converged).to eq 0 + expect(resource.updated?).to be_falsey + expect(converged_recipe.stdout).to eq <<-EOM +* #{resource_name}[blah] action create (up to date) + EOM + end + end + + context "and state1 is set to a new value" do + let(:converge_recipe) { + <<-EOM + #{resource_name} 'blah' do + state1 'new_state1' + end + EOM + } + + it "the resource updates state1" do + expect(resource.converged).to eq 1 + expect(resource.updated?).to be_truthy + expect(converged_recipe.stdout).to eq <<-EOM +* #{resource_name}[blah] action create + - update default_identity1 + - set state1 to "new_state1" (was "current_state1") + EOM + end + end + + context "and state1 and state2 are set to new values" do + let(:converge_recipe) { + <<-EOM + #{resource_name} 'blah' do + state1 'new_state1' + state2 'new_state2' + end + EOM + } + + it "the resource updates state1 and state2" do + expect(resource.converged).to eq 1 + expect(resource.updated?).to be_truthy + expect(converged_recipe.stdout).to eq <<-EOM +* #{resource_name}[blah] action create + - update default_identity1 + - set state1 to "new_state1" (was "current_state1") + - set state2 to "new_state2" (was "current_state2") +EOM + end + end + + context "and state1 is set to its current value but state2 is set to a new value" do + let(:converge_recipe) { + <<-EOM + #{resource_name} 'blah' do + state1 'current_state1' + state2 'new_state2' + end + EOM + } + + it "the resource updates state2" do + expect(resource.converged).to eq 1 + expect(resource.updated?).to be_truthy + expect(converged_recipe.stdout).to eq <<-EOM +* #{resource_name}[blah] action create + - update default_identity1 + - set state2 to "new_state2" (was "current_state2") +EOM + end + end + + context "and state1 and state2 are set to their current values" do + let(:converge_recipe) { + <<-EOM + #{resource_name} 'blah' do + state1 'current_state1' + state2 'current_state2' + end + EOM + } + + it "the resource updates nothing" do + expect(resource.converged).to eq 0 + expect(resource.updated?).to be_falsey + expect(converged_recipe.stdout).to eq <<-EOM +* #{resource_name}[blah] action create (up to date) +EOM + end + end + + context "and identity1 and control1 are set to new values" do + let(:converge_recipe) { + <<-EOM + #{resource_name} 'blah' do + identity1 'new_identity1' + control1 'new_control1' + end + EOM + } + + # Because the identity value is copied over to the new resource, by + # default they do not register as "changed" + it "the resource updates nothing" do + expect(resource.converged).to eq 0 + expect(resource.updated?).to be_falsey + expect(converged_recipe.stdout).to eq <<-EOM +* #{resource_name}[blah] action create (up to date) +EOM + end + end + end + + context "and current_resource with identity1=current, control1=current" do + before :each do + resource_class.load_current_value do + identity1 'current_identity1' + control1 'current_control1' + end + end + + context "and identity1 and control1 are set to new values" do + let(:converge_recipe) { + <<-EOM + #{resource_name} 'blah' do + identity1 'new_identity1' + control1 'new_control1' + end + EOM + } + + # Control values are not desired state and are therefore not considered + # a reason for converging. + it "the resource updates identity1" do + expect(resource.converged).to eq 1 + expect(resource.updated?).to be_truthy + expect(converged_recipe.stdout).to eq <<-EOM +* #{resource_name}[blah] action create + - update current_identity1 + - set identity1 to "new_identity1" (was "current_identity1") + EOM + end + end + end + + context "and has no current_resource" do + before :each do + resource_class.load_current_value do + current_value_does_not_exist! + end + end + + context "and nothing is set" do + let(:converge_recipe) { "#{resource_name} 'blah'" } + + it "the resource is created" do + expect(resource.converged).to eq 1 + expect(resource.updated?).to be_truthy + expect(converged_recipe.stdout).to eq <<-EOM +* #{resource_name}[blah] action create + - create default_identity1 + - set identity1 to "default_identity1" (default value) + - set state1 to "default_state1" (default value) + - set state2 to "default_state2" (default value) +EOM + end + end + + context "and state1 and state2 are set" do + let(:converge_recipe) { + <<-EOM + #{resource_name} 'blah' do + state1 'new_state1' + state2 'new_state2' + end + EOM + } + + it "the resource is created" do + expect(resource.converged).to eq 1 + expect(resource.updated?).to be_truthy + expect(converged_recipe.stdout).to eq <<-EOM +* #{resource_name}[blah] action create + - create default_identity1 + - set identity1 to "default_identity1" (default value) + - set state1 to "new_state1" + - set state2 to "new_state2" +EOM + end + end + end + end + + context "and separate converge_if_changed :state1 and converge_if_changed :state2" do + before :each do + resource_class.action :create do + converge_if_changed :state1 do + new_resource.converged += 1 + end + converge_if_changed :state2 do + new_resource.converged += 1 + end + end + end + + context "and current_resource with state1=current, state2=current" do + before :each do + resource_class.load_current_value do + state1 'current_state1' + state2 'current_state2' + end + end + + context "and nothing is set" do + let(:converge_recipe) { "#{resource_name} 'blah'" } + + it "the resource updates nothing" do + expect(resource.converged).to eq 0 + expect(resource.updated?).to be_falsey + expect(converged_recipe.stdout).to eq <<-EOM +* #{resource_name}[blah] action create (up to date) +EOM + end + end + + context "and state1 is set to a new value" do + + let(:converge_recipe) { + <<-EOM + #{resource_name} 'blah' do + state1 'new_state1' + end + EOM + } + + it "the resource updates state1" do + expect(resource.converged).to eq 1 + expect(resource.updated?).to be_truthy + expect(converged_recipe.stdout).to eq <<-EOM +* #{resource_name}[blah] action create + - update default_identity1 + - set state1 to "new_state1" (was "current_state1") +EOM + end + end + + context "and state1 and state2 are set to new values" do + let(:converge_recipe) { + <<-EOM + #{resource_name} 'blah' do + state1 'new_state1' + state2 'new_state2' + end + EOM + } + + it "the resource updates state1 and state2" do + expect(resource.converged).to eq 2 + expect(resource.updated?).to be_truthy + expect(converged_recipe.stdout).to eq <<-EOM +* #{resource_name}[blah] action create + - update default_identity1 + - set state1 to "new_state1" (was "current_state1") + - update default_identity1 + - set state2 to "new_state2" (was "current_state2") +EOM + end + end + + context "and state1 is set to its current value but state2 is set to a new value" do + let(:converge_recipe) { + <<-EOM + #{resource_name} 'blah' do + state1 'current_state1' + state2 'new_state2' + end + EOM + } + + it "the resource updates state2" do + expect(resource.converged).to eq 1 + expect(resource.updated?).to be_truthy + expect(converged_recipe.stdout).to eq <<-EOM +* #{resource_name}[blah] action create + - update default_identity1 + - set state2 to "new_state2" (was "current_state2") +EOM + end + end + + context "and state1 and state2 are set to their current values" do + let(:converge_recipe) { + <<-EOM + #{resource_name} 'blah' do + state1 'current_state1' + state2 'current_state2' + end + EOM + } + + it "the resource updates nothing" do + expect(resource.converged).to eq 0 + expect(resource.updated?).to be_falsey + expect(converged_recipe.stdout).to eq <<-EOM +* #{resource_name}[blah] action create (up to date) +EOM + end + end + end + + context "and no current_resource" do + before :each do + resource_class.load_current_value do + current_value_does_not_exist! + end + end + + context "and nothing is set" do + let(:converge_recipe) { + "#{resource_name} 'blah'" + } + + it "the resource is created" do + expect(resource.converged).to eq 2 + expect(resource.updated?).to be_truthy + expect(converged_recipe.stdout).to eq <<-EOM +* #{resource_name}[blah] action create + - create default_identity1 + - set state1 to "default_state1" (default value) + - create default_identity1 + - set state2 to "default_state2" (default value) +EOM + end + end + + context "and state1 and state2 are set to new values" do + let(:converge_recipe) { + <<-EOM + #{resource_name} 'blah' do + state1 'new_state1' + state2 'new_state2' + end + EOM + } + + it "the resource is created" do + expect(resource.converged).to eq 2 + expect(resource.updated?).to be_truthy + expect(converged_recipe.stdout).to eq <<-EOM +* #{resource_name}[blah] action create + - create default_identity1 + - set state1 to "new_state1" + - create default_identity1 + - set state2 to "new_state2" +EOM + end + end + end + end + + end +end diff --git a/spec/integration/recipes/resource_load_spec.rb b/spec/integration/recipes/resource_load_spec.rb new file mode 100644 index 0000000000..c29b877b59 --- /dev/null +++ b/spec/integration/recipes/resource_load_spec.rb @@ -0,0 +1,206 @@ +require 'support/shared/integration/integration_helper' + +describe "Resource.load_current_value" do + include IntegrationSupport + + module Namer + extend self + attr_accessor :current_index + def incrementing_value + @incrementing_value += 1 + @incrementing_value + end + attr_writer :incrementing_value + end + + before(:all) { Namer.current_index = 1 } + before { Namer.current_index += 1 } + before { Namer.incrementing_value = 0 } + + let(:resource_name) { :"load_current_value_dsl#{Namer.current_index}" } + let(:resource_class) { + result = Class.new(Chef::Resource) do + def self.to_s; resource_name; end + def self.inspect; resource_name.inspect; end + property :x, default: lazy { "default #{Namer.incrementing_value}" } + def self.created_x=(value) + @created = value + end + def self.created_x + @created + end + action :create do + new_resource.class.created_x = x + end + end + result.resource_name resource_name + result + } + + # Pull on resource_class to initialize it + before { resource_class } + + context "with a resource with load_current_value" do + before :each do + resource_class.load_current_value do + x "loaded #{Namer.incrementing_value} (#{self.class.properties.sort_by { |name,p| name }. + select { |name,p| p.is_set?(self) }. + map { |name,p| "#{name}=#{p.get(self)}" }. + join(", ") })" + end + end + + context "and a resource with x set to a desired value" do + let(:resource) do + e = self + r = nil + converge { + r = public_send(e.resource_name, 'blah') do + x 'desired' + end + } + r + end + + it "current_resource is passed name but not x" do + expect(resource.current_resource.x).to eq 'loaded 2 (name=blah)' + end + + it "resource.current_resource returns a different resource" do + expect(resource.current_resource.x).to eq 'loaded 2 (name=blah)' + expect(resource.x).to eq 'desired' + end + + it "resource.current_resource constructs the resource anew each time" do + expect(resource.current_resource.x).to eq 'loaded 2 (name=blah)' + expect(resource.current_resource.x).to eq 'loaded 3 (name=blah)' + end + + it "the provider accesses the current value of x" do + expect(resource.class.created_x).to eq 'desired' + end + + context "and identity: :i and :d with desired_state: false" do + before { + resource_class.class_eval do + property :i, identity: true + property :d, desired_state: false + end + } + + before { + resource.i 'desired_i' + resource.d 'desired_d' + } + + it "i, name and d are passed to load_current_value, but not x" do + expect(resource.current_resource.x).to eq 'loaded 2 (d=desired_d, i=desired_i, name=blah)' + end + end + + context "and name_property: :i and :d with desired_state: false" do + before { + resource_class.class_eval do + property :i, name_property: true + property :d, desired_state: false + end + } + + before { + resource.i 'desired_i' + resource.d 'desired_d' + } + + it "i, name and d are passed to load_current_value, but not x" do + expect(resource.current_resource.x).to eq 'loaded 2 (d=desired_d, i=desired_i, name=blah)' + end + end + end + + context "and a resource with no values set" do + let(:resource) do + e = self + r = nil + converge { + r = public_send(e.resource_name, 'blah') do + end + } + r + end + + it "the provider accesses values from load_current_value" do + expect(resource.class.created_x).to eq 'loaded 1 (name=blah)' + end + end + + let (:subresource_name) { + :"load_current_value_subresource_dsl#{Namer.current_index}" + } + let (:subresource_class) { + r = Class.new(resource_class) do + property :y, default: lazy { "default_y #{Namer.incrementing_value}" } + end + r.resource_name subresource_name + r + } + + # Pull on subresource_class to initialize it + before { subresource_class } + + let(:subresource) do + e = self + r = nil + converge { + r = public_send(e.subresource_name, 'blah') do + x 'desired' + end + } + r + end + + context "and a child resource class with no load_current_value" do + it "the parent load_current_value is used" do + expect(subresource.current_resource.x).to eq 'loaded 2 (name=blah)' + end + it "load_current_value yields a copy of the child class" do + expect(subresource.current_resource).to be_kind_of(subresource_class) + end + end + + context "And a child resource class with load_current_value" do + before { + subresource_class.load_current_value do + y "loaded_y #{Namer.incrementing_value} (#{self.class.properties.sort_by { |name,p| name }. + select { |name,p| p.is_set?(self) }. + map { |name,p| "#{name}=#{p.get(self)}" }. + join(", ") })" + end + } + + it "the overridden load_current_value is used" do + current_resource = subresource.current_resource + expect(current_resource.x).to eq 'default 3' + expect(current_resource.y).to eq 'loaded_y 2 (name=blah)' + end + end + + context "and a child resource class with load_current_value calling super()" do + before { + subresource_class.load_current_value do + super() + y "loaded_y #{Namer.incrementing_value} (#{self.class.properties.sort_by { |name,p| name }. + select { |name,p| p.is_set?(self) }. + map { |name,p| "#{name}=#{p.get(self)}" }. + join(", ") })" + end + } + + it "the original load_current_value is called as well as the child one" do + current_resource = subresource.current_resource + expect(current_resource.x).to eq 'loaded 3 (name=blah)' + expect(current_resource.y).to eq 'loaded_y 4 (name=blah, x=loaded 3 (name=blah))' + end + end + end + +end diff --git a/spec/integration/solo/solo_spec.rb b/spec/integration/solo/solo_spec.rb index 41f5f5506f..f45933c799 100644 --- a/spec/integration/solo/solo_spec.rb +++ b/spec/integration/solo/solo_spec.rb @@ -15,6 +15,8 @@ describe "chef-solo" do let(:cookbook_ancient_100_metadata_rb) { cb_metadata("ancient", "1.0.0") } + let(:chef_solo) { "ruby bin/chef-solo --minimal-ohai" } + when_the_repository "has a cookbook with a basic recipe" do before do file 'cookbooks/x/metadata.rb', cookbook_x_100_metadata_rb @@ -26,7 +28,7 @@ describe "chef-solo" do cookbook_path "#{path_to('cookbooks')}" file_cache_path "#{path_to('config/cache')}" EOM - result = shell_out("ruby bin/chef-solo -c \"#{path_to('config/solo.rb')}\" -o 'x::default' -l debug", :cwd => chef_dir) + result = shell_out("#{chef_solo} -c \"#{path_to('config/solo.rb')}\" -o 'x::default' -l debug", :cwd => chef_dir) result.error! expect(result.stdout).to include("ITWORKS") end @@ -41,7 +43,7 @@ EOM {"run_list":["x::default"]} E - result = shell_out("ruby bin/chef-solo -c \"#{path_to('config/solo.rb')}\" -j '#{path_to('config/node.json')}' -l debug", :cwd => chef_dir) + result = shell_out("#{chef_solo} -c \"#{path_to('config/solo.rb')}\" -j '#{path_to('config/node.json')}' -l debug", :cwd => chef_dir) result.error! expect(result.stdout).to include("ITWORKS") end @@ -62,7 +64,7 @@ E cookbook_path "#{path_to('cookbooks')}" file_cache_path "#{path_to('config/cache')}" EOM - result = shell_out("ruby bin/chef-solo -c \"#{path_to('config/solo.rb')}\" -o 'x::default' -l debug", :cwd => chef_dir) + result = shell_out("#{chef_solo} -c \"#{path_to('config/solo.rb')}\" -o 'x::default' -l debug", :cwd => chef_dir) expect(result.exitstatus).to eq(0) # For CHEF-5120 this becomes 1 expect(result.stdout).to include("WARN: MissingCookbookDependency") end @@ -95,14 +97,14 @@ EOM chef_dir = File.join(File.dirname(__FILE__), "..", "..", "..") # Instantiate the first chef-solo run - s1 = Process.spawn("ruby bin/chef-solo -c \"#{path_to('config/solo.rb')}\" -o 'x::default' \ + s1 = Process.spawn("#{chef_solo} -c \"#{path_to('config/solo.rb')}\" -o 'x::default' \ -l debug -L #{path_to('logs/runs.log')}", :chdir => chef_dir) # Give it some time to progress sleep 1 # Instantiate the second chef-solo run - s2 = Process.spawn("ruby bin/chef-solo -c \"#{path_to('config/solo.rb')}\" -o 'x::default' \ + s2 = Process.spawn("#{chef_solo} -c \"#{path_to('config/solo.rb')}\" -o 'x::default' \ -l debug -L #{path_to('logs/runs.log')}", :chdir => chef_dir) Process.waitpid(s1) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8888efc424..aadf55f64b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -54,6 +54,9 @@ Dir['lib/chef/knife/**/*.rb']. map {|f| f.gsub(%r[\.rb$], '') }. each {|f| require f } +require 'chef/resource_resolver' +require 'chef/provider_resolver' + require 'chef/mixins' require 'chef/dsl' require 'chef/application' @@ -84,12 +87,14 @@ Dir["spec/support/**/*.rb"]. OHAI_SYSTEM = Ohai::System.new OHAI_SYSTEM.all_plugins("platform") -TEST_PLATFORM = - (OHAI_SYSTEM['platform'] || - 'unknown_test_platform').dup.freeze -TEST_PLATFORM_VERSION = - (OHAI_SYSTEM['platform_version'] || - 'unknown_platform_version').dup.freeze +test_node = Chef::Node.new +test_node.automatic['os'] = (OHAI_SYSTEM['os'] || 'unknown_os').dup.freeze +test_node.automatic['platform_family'] = (OHAI_SYSTEM['platform_family'] || 'unknown_platform_family').dup.freeze +test_node.automatic['platform'] = (OHAI_SYSTEM['platform'] || 'unknown_platform').dup.freeze +test_node.automatic['platform_version'] = (OHAI_SYSTEM['platform_version'] || 'unknown_platform_version').dup.freeze +TEST_NODE = test_node.freeze +TEST_PLATFORM = TEST_NODE['platform'] +TEST_PLATFORM_VERSION = TEST_NODE['platform_version'] RSpec.configure do |config| config.include(Matchers) @@ -110,19 +115,27 @@ RSpec.configure do |config| # Tests that randomly fail, but may have value. config.filter_run_excluding :volatile => true config.filter_run_excluding :volatile_on_solaris => true if solaris? + config.filter_run_excluding :volatile_from_verify => false + + config.filter_run_excluding :skip_appveyor => true if ENV["APPVEYOR"] + config.filter_run_excluding :appveyor_only => true unless ENV["APPVEYOR"] - # Add jruby filters here config.filter_run_excluding :windows_only => true unless windows? config.filter_run_excluding :not_supported_on_mac_osx_106 => true if mac_osx_106? + config.filter_run_excluding :not_supported_on_mac_osx=> true if mac_osx? + config.filter_run_excluding :mac_osx_only=> true if !mac_osx? config.filter_run_excluding :not_supported_on_win2k3 => true if windows_win2k3? config.filter_run_excluding :not_supported_on_solaris => true if solaris? config.filter_run_excluding :win2k3_only => true unless windows_win2k3? config.filter_run_excluding :windows_2008r2_or_later => true unless windows_2008r2_or_later? config.filter_run_excluding :windows64_only => true unless windows64? config.filter_run_excluding :windows32_only => true unless windows32? + config.filter_run_excluding :ruby64_only => true unless ruby_64bit? + config.filter_run_excluding :ruby32_only => true unless ruby_32bit? config.filter_run_excluding :windows_powershell_dsc_only => true unless windows_powershell_dsc? config.filter_run_excluding :windows_powershell_no_dsc_only => true unless ! windows_powershell_dsc? config.filter_run_excluding :windows_domain_joined_only => true unless windows_domain_joined? + config.filter_run_excluding :windows_not_domain_joined_only => true if windows_domain_joined? config.filter_run_excluding :solaris_only => true unless solaris? config.filter_run_excluding :system_windows_service_gem_only => true unless system_windows_service_gem? config.filter_run_excluding :unix_only => true unless unix? @@ -143,7 +156,7 @@ RSpec.configure do |config| config.filter_run_excluding :aes_256_gcm_only => true unless aes_256_gcm? config.filter_run_excluding :broken => true - running_platform_arch = `uname -m`.strip + running_platform_arch = `uname -m`.strip unless windows? config.filter_run_excluding :arch => lambda {|target_arch| running_platform_arch != target_arch @@ -154,13 +167,17 @@ RSpec.configure do |config| config.filter_run_excluding :provider => lambda {|criteria| type, target_provider = criteria.first - platform = TEST_PLATFORM.dup - platform_version = TEST_PLATFORM_VERSION.dup - - begin - provider_for_running_platform = Chef::Platform.find_provider(platform, platform_version, type) - provider_for_running_platform != target_provider - rescue ArgumentError # no provider for platform + node = TEST_NODE.dup + resource_class = Chef::ResourceResolver.resolve(type, node: node) + if resource_class + resource = resource_class.new('test', Chef::RunContext.new(node, nil, nil)) + begin + provider = resource.provider_for_action(Array(resource_class.default_action).first) + provider.class != target_provider + rescue Chef::Exceptions::ProviderNotFound # no provider for platform + true + end + else true end } @@ -168,6 +185,8 @@ RSpec.configure do |config| config.run_all_when_everything_filtered = true config.before(:each) do + Chef.reset! + Chef::Config.reset # By default, treat deprecation warnings as errors in tests. diff --git a/spec/support/key_helpers.rb b/spec/support/key_helpers.rb new file mode 100644 index 0000000000..076f709380 --- /dev/null +++ b/spec/support/key_helpers.rb @@ -0,0 +1,104 @@ +# +# Author:: Tyler Cloke (<tyler@chef.io>) +# 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' + +shared_examples_for "a knife key command" do + let(:stderr) { StringIO.new } + let(:command) do + c = described_class.new([]) + c.ui.config[:disable_editing] = true + allow(c.ui).to receive(:stderr).and_return(stderr) + allow(c.ui).to receive(:stdout).and_return(stderr) + allow(c).to receive(:show_usage) + c + end + + context "before apply_params! is called" do + context "when apply_params! is called with invalid args (missing actor)" do + let(:params) { [] } + it "shows the usage" do + expect(command).to receive(:show_usage) + expect { command.apply_params!(params) }.to exit_with_code(1) + end + + it "outputs the proper error" do + expect { command.apply_params!(params) }.to exit_with_code(1) + expect(stderr.string).to include(command.actor_missing_error) + end + + it "exits 1" do + expect { command.apply_params!(params) }.to exit_with_code(1) + end + end + end # before apply_params! is called + + context "after apply_params! is called with valid args" do + before do + command.apply_params!(params) + end + + it "properly defines the actor" do + expect(command.actor).to eq("charmander") + end + end # after apply_params! is called with valid args + + context "when the command is run" do + before do + allow(command).to receive(:service_object).and_return(service_object) + allow(command).to receive(:name_args).and_return(["charmander"]) + end + + context "when the command is successful" do + before do + expect(service_object).to receive(:run) + end + end + end +end # a knife key command + +shared_examples_for "a knife key command with a keyname as the second arg" do + let(:stderr) { StringIO.new } + let(:command) do + c = described_class.new([]) + c.ui.config[:disable_editing] = true + allow(c.ui).to receive(:stderr).and_return(stderr) + allow(c.ui).to receive(:stdout).and_return(stderr) + allow(c).to receive(:show_usage) + c + end + + context "before apply_params! is called" do + context "when apply_params! is called with invalid args (missing keyname)" do + let(:params) { ["charmander"] } + it "shows the usage" do + expect(command).to receive(:show_usage) + expect { command.apply_params!(params) }.to exit_with_code(1) + end + + it "outputs the proper error" do + expect { command.apply_params!(params) }.to exit_with_code(1) + expect(stderr.string).to include(command.keyname_missing_error) + end + + it "exits 1" do + expect { command.apply_params!(params) }.to exit_with_code(1) + end + end + end # before apply_params! is called +end diff --git a/spec/support/lib/chef/provider/openldap_includer.rb b/spec/support/lib/chef/provider/openldap_includer.rb new file mode 100644 index 0000000000..afb0c7cf01 --- /dev/null +++ b/spec/support/lib/chef/provider/openldap_includer.rb @@ -0,0 +1,29 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, 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. +# + +class Chef + class Provider + class OpenldapIncluder < Chef::Provider::LWRPBase + provides :openldap_includer + + def action_run + include_recipe "openldap::default" + end + end + end +end diff --git a/spec/support/lib/chef/resource/cat.rb b/spec/support/lib/chef/resource/cat.rb index ecca50cb53..efc78aa59c 100644 --- a/spec/support/lib/chef/resource/cat.rb +++ b/spec/support/lib/chef/resource/cat.rb @@ -23,7 +23,6 @@ class Chef attr_accessor :action def initialize(name, run_context=nil) - @resource_name = :cat super @action = "sell" end diff --git a/spec/support/lib/chef/resource/one_two_three_four.rb b/spec/support/lib/chef/resource/one_two_three_four.rb index 296d2cd970..8f273a0cda 100644 --- a/spec/support/lib/chef/resource/one_two_three_four.rb +++ b/spec/support/lib/chef/resource/one_two_three_four.rb @@ -19,12 +19,8 @@ class Chef class Resource class OneTwoThreeFour < Chef::Resource - attr_reader :i_can_count - def initialize(name, run_context) - @resource_name = :one_two_three_four - super - end + attr_reader :i_can_count def i_can_count(tf) @i_can_count = tf diff --git a/spec/support/lib/chef/resource/openldap_includer.rb b/spec/support/lib/chef/resource/openldap_includer.rb new file mode 100644 index 0000000000..6f443b4c7c --- /dev/null +++ b/spec/support/lib/chef/resource/openldap_includer.rb @@ -0,0 +1,27 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008, 2010 Opscode, 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. +# + + +class Chef + class Resource + class OpenldapIncluder < Chef::Resource::LWRPBase + allowed_actions :run + default_action :run + end + end +end diff --git a/spec/support/lib/chef/resource/with_state.rb b/spec/support/lib/chef/resource/with_state.rb index 226de0a6d2..773ae7ddb8 100644 --- a/spec/support/lib/chef/resource/with_state.rb +++ b/spec/support/lib/chef/resource/with_state.rb @@ -23,15 +23,6 @@ class Chef class Resource class WithState < Chef::Resource attr_accessor :state - - def initialize(name, run_context=nil) - @resource_name = :with_state - super - end - - def state - @state - end end end end diff --git a/spec/support/lib/chef/resource/zen_follower.rb b/spec/support/lib/chef/resource/zen_follower.rb index ddc289e48d..155e6ae729 100644 --- a/spec/support/lib/chef/resource/zen_follower.rb +++ b/spec/support/lib/chef/resource/zen_follower.rb @@ -24,11 +24,6 @@ class Chef provides :follower, platform: "zen" - def initialize(name, run_context=nil) - @resource_name = :zen_follower - super - end - def master(arg=nil) if !arg.nil? @master = arg diff --git a/spec/support/lib/chef/resource/zen_master.rb b/spec/support/lib/chef/resource/zen_master.rb index d47d174e28..4106549d79 100644 --- a/spec/support/lib/chef/resource/zen_master.rb +++ b/spec/support/lib/chef/resource/zen_master.rb @@ -22,13 +22,10 @@ require 'chef/json_compat' class Chef class Resource class ZenMaster < Chef::Resource + allowed_actions :win, :score + attr_reader :peace - def initialize(name, run_context=nil) - @resource_name = :zen_master - super - allowed_actions << :win << :score - end def peace(tf) @peace = tf diff --git a/spec/support/mock/platform.rb b/spec/support/mock/platform.rb index ab2c19baff..7eae82fe7d 100644 --- a/spec/support/mock/platform.rb +++ b/spec/support/mock/platform.rb @@ -6,7 +6,7 @@ # testing code that mixes in platform specific modules like +Chef::Mixin::Securable+ # or +Chef::FileAccessControl+ def platform_mock(platform = :unix, &block) - allow(Chef::Platform).to receive(:windows?).and_return(platform == :windows ? true : false) + allow(ChefConfig).to receive(:windows?).and_return(platform == :windows ? true : false) ENV['SYSTEMDRIVE'] = (platform == :windows ? 'C:' : nil) if platform == :windows diff --git a/spec/support/platform_helpers.rb b/spec/support/platform_helpers.rb index a412fe38e1..1cfad05172 100644 --- a/spec/support/platform_helpers.rb +++ b/spec/support/platform_helpers.rb @@ -26,6 +26,14 @@ def ruby_20? !!(RUBY_VERSION =~ /^2.0/) end +def ruby_64bit? + !!(RbConfig::CONFIG['host_cpu'] =~ /x86_64/) +end + +def ruby_32bit? + !!(RbConfig::CONFIG['host_cpu'] =~ /i686/) +end + def windows? !!(RUBY_PLATFORM =~ /mswin|mingw|windows/) end @@ -88,6 +96,20 @@ def mac_osx_106? false end +def mac_osx? + if File.exists? "/usr/bin/sw_vers" + result = ShellHelpers.shell_out("/usr/bin/sw_vers") + result.stdout.each_line do |line| + if line =~ /^ProductName:\sMac OS X.*$/ + return true + end + end + end + + false +end + + # detects if the hardware is 64-bit (evaluates to true in "WOW64" mode in a 32-bit app on a 64-bit system) def windows64? windows? && ( ENV['PROCESSOR_ARCHITECTURE'] == 'AMD64' || ENV['PROCESSOR_ARCHITEW6432'] == 'AMD64' ) diff --git a/spec/support/shared/context/client.rb b/spec/support/shared/context/client.rb new file mode 100644 index 0000000000..eb537e9889 --- /dev/null +++ b/spec/support/shared/context/client.rb @@ -0,0 +1,277 @@ + +require 'spec_helper' + +# Stubs a basic client object +shared_context "client" do + let(:fqdn) { "hostname.example.org" } + let(:hostname) { "hostname" } + let(:machinename) { "machinename.example.org" } + let(:platform) { "example-platform" } + let(:platform_version) { "example-platform-1.0" } + + let(:ohai_data) do + { + :fqdn => fqdn, + :hostname => hostname, + :machinename => machinename, + :platform => platform, + :platform_version => platform_version + } + end + + let(:ohai_system) do + ohai = instance_double("Ohai::System", :all_plugins => true, :data => ohai_data) + allow(ohai).to receive(:[]) do |k| + ohai_data[k] + end + ohai + end + + let(:node) do + Chef::Node.new.tap do |n| + n.name(fqdn) + n.chef_environment("_default") + end + end + + let(:json_attribs) { nil } + let(:client_opts) { {} } + + let(:client) do + Chef::Config[:event_loggers] = [] + Chef::Client.new(json_attribs, client_opts).tap do |c| + c.node = node + end + end + + before do + Chef::Log.logger = Logger.new(StringIO.new) + + # Node/Ohai data + #Chef::Config[:node_name] = fqdn + allow(Ohai::System).to receive(:new).and_return(ohai_system) + end +end + +# Stubs a client for a client run. +# Requires a client object be defined in the scope of this included context. +# e.g.: +# describe "some functionality" do +# include_context "client" +# include_context "a client run" +# ... +# end +shared_context "a client run" do + let(:stdout) { StringIO.new } + let(:stderr) { StringIO.new } + + let(:api_client_exists?) { false } + let(:enable_fork) { false } + + let(:http_cookbook_sync) { double("Chef::REST (cookbook sync)") } + let(:http_node_load) { double("Chef::REST (node)") } + let(:http_node_save) { double("Chef::REST (node save)") } + + let(:runner) { instance_double("Chef::Runner") } + let(:audit_runner) { instance_double("Chef::Audit::Runner", :failed? => false) } + + def stub_for_register + # --Client.register + # Make sure Client#register thinks the client key doesn't + # exist, so it tries to register and create one. + allow(File).to receive(:exists?).and_call_original + expect(File).to receive(:exists?). + with(Chef::Config[:client_key]). + exactly(:once). + and_return(api_client_exists?) + + unless api_client_exists? + # Client.register will register with the validation client name. + expect_any_instance_of(Chef::ApiClient::Registration).to receive(:run) + end + end + + def stub_for_node_load + # Client.register will then turn around create another + # Chef::REST object, this time with the client key it got from the + # previous step. + expect(Chef::REST).to receive(:new). + with(Chef::Config[:chef_server_url], fqdn, Chef::Config[:client_key]). + exactly(:once). + and_return(http_node_load) + + # --Client#build_node + # looks up the node, which we will return, then later saves it. + expect(Chef::Node).to receive(:find_or_create).with(fqdn).and_return(node) + + # --ResourceReporter#node_load_completed + # gets a run id from the server for storing resource history + # (has its own tests, so stubbing it here.) + expect_any_instance_of(Chef::ResourceReporter).to receive(:node_load_completed) + end + + def stub_for_sync_cookbooks + # --Client#setup_run_context + # ---Client#sync_cookbooks -- downloads the list of cookbooks to sync + # + expect_any_instance_of(Chef::CookbookSynchronizer).to receive(:sync_cookbooks) + expect(Chef::REST).to receive(:new).with(Chef::Config[:chef_server_url]).and_return(http_cookbook_sync) + expect(http_cookbook_sync).to receive(:post). + with("environments/_default/cookbook_versions", {:run_list => []}). + and_return({}) + end + + def stub_for_converge + # define me + end + + def stub_for_audit + # define me + end + + def stub_for_node_save + # define me + end + + def stub_for_run + # define me + end + + before do + Chef::Config[:client_fork] = enable_fork + Chef::Config[:cache_path] = windows? ? 'C:\chef' : '/var/chef' + Chef::Config[:why_run] = false + Chef::Config[:audit_mode] = :enabled + + stub_const("Chef::Client::STDOUT_FD", stdout) + stub_const("Chef::Client::STDERR_FD", stderr) + + stub_for_register + stub_for_node_load + stub_for_sync_cookbooks + stub_for_converge + stub_for_audit + stub_for_node_save + + expect_any_instance_of(Chef::RunLock).to receive(:acquire) + expect_any_instance_of(Chef::RunLock).to receive(:save_pid) + expect_any_instance_of(Chef::RunLock).to receive(:release) + + # Post conditions: check that node has been filled in correctly + expect(client).to receive(:run_started) + + stub_for_run + end +end + +shared_context "converge completed" do + def stub_for_converge + # --Client#converge + expect(Chef::Runner).to receive(:new).and_return(runner) + expect(runner).to receive(:converge).and_return(true) + end + + def stub_for_node_save + allow(node).to receive(:data_for_save).and_return(node.for_json) + + # --Client#save_updated_node + expect(Chef::REST).to receive(:new).with(Chef::Config[:chef_server_url], fqdn, Chef::Config[:client_key], validate_utf8: false).and_return(http_node_save) + expect(http_node_save).to receive(:put_rest).with("nodes/#{fqdn}", node.for_json).and_return(true) + end +end + +shared_context "converge failed" do + let(:converge_error) do + err = Chef::Exceptions::UnsupportedAction.new("Action unsupported") + err.set_backtrace([ "/path/recipe.rb:15", "/path/recipe.rb:12" ]) + err + end + + def stub_for_converge + expect(Chef::Runner).to receive(:new).and_return(runner) + expect(runner).to receive(:converge).and_raise(converge_error) + end + + def stub_for_node_save + expect(client).to_not receive(:save_updated_node) + end +end + +shared_context "audit phase completed" do + def stub_for_audit + # -- Client#run_audits + expect(Chef::Audit::Runner).to receive(:new).and_return(audit_runner) + expect(audit_runner).to receive(:run).and_return(true) + expect(client.events).to receive(:audit_phase_complete) + end +end + +shared_context "audit phase failed with error" do + let(:audit_error) do + err = RuntimeError.new("Unexpected audit error") + err.set_backtrace([ "/path/recipe.rb:57", "/path/recipe.rb:55" ]) + err + end + + def stub_for_audit + expect(Chef::Audit::Runner).to receive(:new).and_return(audit_runner) + expect(Chef::Audit::Logger).to receive(:read_buffer).and_return("Audit mode output!") + expect(audit_runner).to receive(:run).and_raise(audit_error) + expect(client.events).to receive(:audit_phase_failed).with(audit_error, "Audit mode output!") + end +end + +shared_context "audit phase completed with failed controls" do + let(:audit_runner) { instance_double("Chef::Audit::Runner", :failed? => true, + :num_failed => 1, :num_total => 3) } + + let(:audit_error) do + err = Chef::Exceptions::AuditsFailed.new(audit_runner.num_failed, audit_runner.num_total) + err.set_backtrace([ "/path/recipe.rb:108", "/path/recipe.rb:103" ]) + err + end + + def stub_for_audit + expect(Chef::Audit::Runner).to receive(:new).and_return(audit_runner) + expect(Chef::Audit::Logger).to receive(:read_buffer).and_return("Audit mode output!") + expect(audit_runner).to receive(:run) + expect(Chef::Exceptions::AuditsFailed).to receive(:new).with( + audit_runner.num_failed, audit_runner.num_total + ).and_return(audit_error) + expect(client.events).to receive(:audit_phase_failed).with(audit_error, "Audit mode output!") + end +end + +shared_context "run completed" do + def stub_for_run + expect(client).to receive(:run_completed_successfully) + + # --ResourceReporter#run_completed + # updates the server with the resource history + # (has its own tests, so stubbing it here.) + expect_any_instance_of(Chef::ResourceReporter).to receive(:run_completed) + # --AuditReporter#run_completed + # posts the audit data to server. + # (has its own tests, so stubbing it here.) + expect_any_instance_of(Chef::Audit::AuditReporter).to receive(:run_completed) + end +end + +shared_context "run failed" do + def stub_for_run + expect(client).to receive(:run_failed) + + # --ResourceReporter#run_completed + # updates the server with the resource history + # (has its own tests, so stubbing it here.) + expect_any_instance_of(Chef::ResourceReporter).to receive(:run_failed) + # --AuditReporter#run_completed + # posts the audit data to server. + # (has its own tests, so stubbing it here.) + expect_any_instance_of(Chef::Audit::AuditReporter).to receive(:run_failed) + end + + before do + expect(Chef::Application).to receive(:debug_stacktrace).with an_instance_of(Chef::Exceptions::RunFailedWrappingError) + end +end diff --git a/spec/support/shared/examples/client.rb b/spec/support/shared/examples/client.rb new file mode 100644 index 0000000000..330cb40ac6 --- /dev/null +++ b/spec/support/shared/examples/client.rb @@ -0,0 +1,53 @@ + +require 'spec_helper' +require 'spec/support/shared/context/client' + +# requires platform and platform_version be defined +shared_examples "a completed run" do + include_context "run completed" + + it "runs ohai, sets up authentication, loads node state, synchronizes policy, converges, and runs audits" do + # This is what we're testing. + expect(client.run).to be true + + # fork is stubbed, so we can see the outcome of the run + expect(node.automatic_attrs[:platform]).to eq(platform) + expect(node.automatic_attrs[:platform_version]).to eq(platform_version) + end +end + +shared_examples "a completed run with audit failure" do + include_context "run completed" + + before do + expect(Chef::Application).to receive(:debug_stacktrace).with an_instance_of(Chef::Exceptions::RunFailedWrappingError) + end + + it "converges, runs audits, saves the node and raises the error in a wrapping error" do + expect{ client.run }.to raise_error(Chef::Exceptions::RunFailedWrappingError) do |error| + expect(error.wrapped_errors.size).to eq(run_errors.size) + run_errors.each do |run_error| + expect(error.wrapped_errors).to include(run_error) + expect(error.backtrace).to include(*run_error.backtrace) + end + end + + # fork is stubbed, so we can see the outcome of the run + expect(node.automatic_attrs[:platform]).to eq(platform) + expect(node.automatic_attrs[:platform_version]).to eq(platform_version) + end +end + +shared_examples "a failed run" do + include_context "run failed" + + it "skips node save and raises the error in a wrapping error" do + expect{ client.run }.to raise_error(Chef::Exceptions::RunFailedWrappingError) do |error| + expect(error.wrapped_errors.size).to eq(run_errors.size) + run_errors.each do |run_error| + expect(error.wrapped_errors).to include(run_error) + expect(error.backtrace).to include(*run_error.backtrace) + end + end + end +end diff --git a/spec/support/shared/functional/file_resource.rb b/spec/support/shared/functional/file_resource.rb index 4f8e2f5b71..3ce3c9c94e 100644 --- a/spec/support/shared/functional/file_resource.rb +++ b/spec/support/shared/functional/file_resource.rb @@ -592,10 +592,6 @@ shared_examples_for "a configured file resource" do File.open(path, "wb") { |f| f.write(wrong_content) } end - it "updates the source file content" do - skip - end - it "marks the resource as updated" do resource.run_action(:create) expect(resource).to be_updated_by_last_action diff --git a/spec/support/shared/functional/securable_resource.rb b/spec/support/shared/functional/securable_resource.rb index e016bb685d..b3c32356aa 100644 --- a/spec/support/shared/functional/securable_resource.rb +++ b/spec/support/shared/functional/securable_resource.rb @@ -163,9 +163,6 @@ shared_examples_for "a securable resource with existing target" do let(:desired_gid) { 1337 } let(:expected_gid) { 1337 } - skip "should set an owner (Rerun specs under root)", :requires_unprivileged_user => true - skip "should set a group (Rerun specs under root)", :requires_unprivileged_user => true - describe "when setting the owner", :requires_root do before do resource.owner expected_user_name @@ -205,11 +202,6 @@ shared_examples_for "a securable resource with existing target" do resource.run_action(:create) end - it "should set permissions as specified" do - pending("Linux does not support lchmod") - expect{ File.lstat(path).mode & 007777 }.to eq(@mode_string.oct & 007777) - end - it "is marked as updated only if changes are made" do expect(resource.updated_by_last_action?).to eq(expect_updated?) end @@ -222,15 +214,28 @@ shared_examples_for "a securable resource with existing target" do resource.run_action(:create) end - it "should set permissions in numeric form as a ruby-interpreted octal" do - pending('Linux does not support lchmod') - expect{ File.lstat(path).mode & 007777 }.to eq(@mode_integer & 007777) - end - it "is marked as updated only if changes are made" do expect(resource.updated_by_last_action?).to eq(expect_updated?) end end + + describe "when setting the suid bit", :requires_root do + before do + @suid_mode = 04776 + resource.mode @suid_mode + resource.run_action(:create) + end + + it "should set the suid bit" do + expect(File.lstat(path).mode & 007777).to eq(@suid_mode & 007777) + end + + it "should retain the suid bit when updating the user" do + resource.user 1338 + resource.run_action(:create) + expect(File.lstat(path).mode & 007777).to eq(@suid_mode & 007777) + end + end end context "on Windows", :windows_only do @@ -288,17 +293,13 @@ shared_examples_for "a securable resource without existing target" do include_context "diff disabled" - context "on Unix", :unix_only do - skip "if we need any securable resource tests on Unix without existing target resource." - end - context "on Windows", :windows_only do include_context "use Windows permissions" - it "sets owner to Administrators on create if owner is not specified" do + it "leaves owner as system default on create if owner is not specified" do expect(File.exist?(path)).to eq(false) resource.run_action(:create) - expect(descriptor.owner).to eq(SID.Administrators) + expect(descriptor.owner).to eq(SID.default_security_object_owner) end it "sets owner when owner is specified" do @@ -318,22 +319,24 @@ shared_examples_for "a securable resource without existing target" do end it "leaves owner alone if owner is not specified and resource already exists" do - # Set owner to Guest so it's not the same as the current user (which is the default on create) - resource.owner 'Guest' + arbitrary_non_default_owner = SID.Guest + expect(arbitrary_non_default_owner).not_to eq(SID.default_security_object_owner) + + resource.owner 'Guest' # Change to arbitrary_non_default_owner once issue #1508 is fixed resource.run_action(:create) - expect(descriptor.owner).to eq(SID.Guest) + expect(descriptor.owner).to eq(arbitrary_non_default_owner) new_resource = create_resource expect(new_resource.owner).to eq(nil) new_resource.run_action(:create) - expect(descriptor.owner).to eq(SID.Guest) + expect(descriptor.owner).to eq(arbitrary_non_default_owner) end - it "sets group to None on create if group is not specified" do + it "leaves group as system default on create if group is not specified" do expect(resource.group).to eq(nil) expect(File.exist?(path)).to eq(false) resource.run_action(:create) - expect(descriptor.group).to eq(SID.None) + expect(descriptor.group).to eq(SID.default_security_object_group) end it "sets group when group is specified" do @@ -346,23 +349,18 @@ shared_examples_for "a securable resource without existing target" do expect { resource.group 'Lance "The Nose" Glindenberry III' }.to raise_error(Chef::Exceptions::ValidationFailed) end - it "sets group when group is specified with a \\" do - pending("Need to find a group containing a backslash that is on most peoples' machines") - resource.group "#{ENV['COMPUTERNAME']}\\Administrators" - resource.run_action(:create) - expect{ descriptor.group }.to eq(SID.Everyone) - end - it "leaves group alone if group is not specified and resource already exists" do - # Set group to Everyone so it's not the default (None) - resource.group 'Everyone' + arbitrary_non_default_group = SID.Everyone + expect(arbitrary_non_default_group).not_to eq(SID.default_security_object_group) + + resource.group 'Everyone' # Change to arbitrary_non_default_group once issue #1508 is fixed resource.run_action(:create) - expect(descriptor.group).to eq(SID.Everyone) + expect(descriptor.group).to eq(arbitrary_non_default_group) new_resource = create_resource expect(new_resource.group).to eq(nil) new_resource.run_action(:create) - expect(descriptor.group).to eq(SID.Everyone) + expect(descriptor.group).to eq(arbitrary_non_default_group) end describe "with rights and deny_rights attributes" do diff --git a/spec/support/shared/functional/securable_resource_with_reporting.rb b/spec/support/shared/functional/securable_resource_with_reporting.rb index 8a2ceed837..3176ebba0d 100644 --- a/spec/support/shared/functional/securable_resource_with_reporting.rb +++ b/spec/support/shared/functional/securable_resource_with_reporting.rb @@ -35,7 +35,7 @@ shared_examples_for "a securable resource with reporting" do # Default mode varies based on implementation. Providers that use a tempfile # will default to 0600. Providers that use File.open will default to 0666 - # umask - # let(:default_mode) { ((0100666 - File.umask) & 07777).to_s(8) } + # let(:default_mode) { (0666 & ~File.umask).to_s(8) } describe "reading file security metadata for reporting on unix", :unix_only => true do # According to POSIX standard created files get either the @@ -185,7 +185,7 @@ shared_examples_for "a securable resource with reporting" do # TODO: most stable way to specify? expect(current_resource.owner).to eq(Etc.getpwuid(Process.uid).name) expect(current_resource.group).to eq(@expected_group_name) - expect(current_resource.mode).to eq("0#{((0100666 - File.umask) & 07777).to_s(8)}") + expect(current_resource.mode).to eq("0#{(0666 & ~File.umask).to_s(8)}") end end @@ -239,8 +239,8 @@ shared_examples_for "a securable resource with reporting" do end context "and mode is specified as a String" do - let(:default_create_mode) { (0100666 - File.umask) } - let(:expected_mode) { "0#{(default_create_mode & 07777).to_s(8)}" } + let(:default_create_mode) { 0666 & ~File.umask } + let(:expected_mode) { "0#{default_create_mode.to_s(8)}" } before do resource.mode(expected_mode) @@ -252,7 +252,7 @@ shared_examples_for "a securable resource with reporting" do end context "and mode is specified as an Integer" do - let(:set_mode) { (0100666 - File.umask) & 07777 } + let(:set_mode) { 0666 & ~File.umask } let(:expected_mode) { "0#{set_mode.to_s(8)}" } before do @@ -279,14 +279,14 @@ shared_examples_for "a securable resource with reporting" do end it "has empty values for file metadata in 'current_resource'" do - pending "windows reporting not yet fully supported" + skip "windows reporting not yet fully supported" expect(current_resource.owner).to be_nil expect(current_resource.expanded_rights).to be_nil end context "and no security metadata is specified in new_resource" do before do - pending "windows reporting not yet fully supported" + skip "windows reporting not yet fully supported" end it "sets the metadata values on the new_resource as strings after creating" do @@ -322,7 +322,7 @@ shared_examples_for "a securable resource with reporting" do let(:expected_user_name) { 'domain\user' } before do - pending "windows reporting not yet fully supported" + skip "windows reporting not yet fully supported" resource.owner(expected_user_name) resource.run_action(:create) end @@ -336,7 +336,7 @@ shared_examples_for "a securable resource with reporting" do context "when the target file exists" do before do - pending "windows reporting not yet fully supported" + skip "windows reporting not yet fully supported" FileUtils.touch(resource.path) resource.action(:create) end diff --git a/spec/support/shared/functional/win32_service.rb b/spec/support/shared/functional/win32_service.rb index 7dd1920418..2ee1a8ad88 100644 --- a/spec/support/shared/functional/win32_service.rb +++ b/spec/support/shared/functional/win32_service.rb @@ -46,7 +46,8 @@ shared_context "using Win32::Service" do :service_name => "spec-service", :service_display_name => "Spec Test Service", :service_description => "Service for testing Chef::Application::WindowsServiceManager.", - :service_file_path => File.expand_path(File.join(File.dirname(__FILE__), '../../platforms/win32/spec_service.rb')) + :service_file_path => File.expand_path(File.join(File.dirname(__FILE__), '../../platforms/win32/spec_service.rb')), + :delayed_start => true } } diff --git a/spec/support/shared/functional/windows_script.rb b/spec/support/shared/functional/windows_script.rb index 35b86dc4e8..3499cc98ec 100644 --- a/spec/support/shared/functional/windows_script.rb +++ b/spec/support/shared/functional/windows_script.rb @@ -114,7 +114,7 @@ shared_context Chef::Resource::WindowsScript do describe "when the run action is invoked on Windows" do it "executes the script code" do - resource.code("@whoami > #{script_output_path}") + resource.code("whoami > #{script_output_path}") resource.returns(0) resource.run_action(:run) end diff --git a/spec/support/shared/integration/integration_helper.rb b/spec/support/shared/integration/integration_helper.rb index e6942c62af..927ff2f42b 100644 --- a/spec/support/shared/integration/integration_helper.rb +++ b/spec/support/shared/integration/integration_helper.rb @@ -22,14 +22,19 @@ require 'fileutils' require 'chef/config' require 'chef/json_compat' require 'chef/server_api' -require 'chef_zero/rspec' require 'support/shared/integration/knife_support' require 'support/shared/integration/app_server_support' +require 'cheffish/rspec/chef_run_support' require 'spec_helper' module IntegrationSupport include ChefZero::RSpec + def self.included(includer_class) + includer_class.extend(Cheffish::RSpec::ChefRunSupport) + includer_class.extend(ClassMethods) + end + module ClassMethods include ChefZero::RSpec @@ -49,10 +54,6 @@ module IntegrationSupport end end - def self.included(includer_class) - includer_class.extend(ClassMethods) - end - def api Chef::ServerAPI.new end diff --git a/spec/support/shared/shared_examples.rb b/spec/support/shared/shared_examples.rb index b20c65f8b6..550fa2eb68 100644 --- a/spec/support/shared/shared_examples.rb +++ b/spec/support/shared/shared_examples.rb @@ -1,7 +1,7 @@ # For storing any examples shared between multiple tests # Any object which defines a .to_json should import this test -shared_examples "to_json equalivent to Chef::JSONCompat.to_json" do +shared_examples "to_json equivalent to Chef::JSONCompat.to_json" do let(:jsonable) { raise "You must define the subject when including this test" diff --git a/spec/support/shared/unit/api_versioning.rb b/spec/support/shared/unit/api_versioning.rb new file mode 100644 index 0000000000..05a4117f8e --- /dev/null +++ b/spec/support/shared/unit/api_versioning.rb @@ -0,0 +1,77 @@ +# +# Author:: Tyler Cloke (<tyler@chef.io>) +# 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 "chef/exceptions" + +shared_examples_for "version handling" do + let(:response_406) { OpenStruct.new(:code => '406') } + let(:exception_406) { Net::HTTPServerException.new("406 Not Acceptable", response_406) } + + before do + allow(rest_v1).to receive(http_verb).and_raise(exception_406) + end + + context "when the server does not support the min or max server API version that Chef::UserV1 supports" do + before do + allow(object).to receive(:server_client_api_version_intersection).and_return([]) + end + + it "raises the original exception" do + expect{ object.send(method) }.to raise_error(exception_406) + end + end # when the server does not support the min or max server API version that Chef::UserV1 supports +end # version handling + +shared_examples_for "user and client reregister" do + let(:response_406) { OpenStruct.new(:code => '406') } + let(:exception_406) { Net::HTTPServerException.new("406 Not Acceptable", response_406) } + let(:generic_exception) { Exception.new } + let(:min_version) { "2" } + let(:max_version) { "5" } + let(:return_hash_406) { + { + "min_version" => min_version, + "max_version" => max_version, + "request_version" => "30" + } + } + + context "when V0 is not supported by the server" do + context "when the exception is 406 and returns x-ops-server-api-version header" do + before do + allow(rest_v0).to receive(:put).and_raise(exception_406) + allow(response_406).to receive(:[]).with('x-ops-server-api-version').and_return(Chef::JSONCompat.to_json(return_hash_406)) + end + + it "raises an error about only V0 being supported" do + expect(object).to receive(:reregister_only_v0_supported_error_msg).with(max_version, min_version) + expect{ object.reregister }.to raise_error(Chef::Exceptions::OnlyApiVersion0SupportedForAction) + end + + end + context "when the exception is not versioning related" do + before do + allow(rest_v0).to receive(:put).and_raise(generic_exception) + end + + it "raises the original error" do + expect{ object.reregister }.to raise_error(generic_exception) + end + end + end +end diff --git a/spec/support/shared/unit/knife_shared.rb b/spec/support/shared/unit/knife_shared.rb new file mode 100644 index 0000000000..8c9010f3cf --- /dev/null +++ b/spec/support/shared/unit/knife_shared.rb @@ -0,0 +1,40 @@ +# +# Author:: Tyler Cloke (<tyler@chef.io>) +# 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. +# + + +shared_examples_for "mandatory field missing" do + context "when field is nil" do + before do + knife.name_args = name_args + end + + it "exits 1" do + expect { knife.run }.to raise_error(SystemExit) + end + + it "prints the usage" do + expect(knife).to receive(:show_usage) + expect { knife.run }.to raise_error(SystemExit) + end + + it "prints a relevant error message" do + expect { knife.run }.to raise_error(SystemExit) + expect(stderr.string).to match /You must specify a #{fieldname}/ + end + end +end diff --git a/spec/support/shared/unit/provider/file.rb b/spec/support/shared/unit/provider/file.rb index 86f32c9e89..7de9698451 100644 --- a/spec/support/shared/unit/provider/file.rb +++ b/spec/support/shared/unit/provider/file.rb @@ -255,7 +255,7 @@ shared_examples_for Chef::Provider::File do context "examining file security metadata on Unix with a file that exists" do before do # fake that we're on unix even if we're on windows - allow(Chef::Platform).to receive(:windows?).and_return(false) + allow(ChefConfig).to receive(:windows?).and_return(false) # mock up the filesystem to behave like unix setup_normal_file stat_struct = double("::File.stat", :mode => 0600, :uid => 0, :gid => 0, :mtime => 10000) @@ -331,7 +331,7 @@ shared_examples_for Chef::Provider::File do context "examining file security metadata on Unix with a file that does not exist" do before do # fake that we're on unix even if we're on windows - allow(Chef::Platform).to receive(:windows?).and_return(false) + allow(ChefConfig).to receive(:windows?).and_return(false) setup_missing_file end @@ -380,7 +380,7 @@ shared_examples_for Chef::Provider::File do before do # fake that we're on unix even if we're on windows - allow(Chef::Platform).to receive(:windows?).and_return(false) + allow(ChefConfig).to receive(:windows?).and_return(false) # mock up the filesystem to behave like unix setup_normal_file stat_struct = double("::File.stat", :mode => 0600, :uid => 0, :gid => 0, :mtime => 10000) @@ -529,26 +529,49 @@ shared_examples_for Chef::Provider::File do :for_reporting => diff_for_reporting ) allow(diff).to receive(:diff).with(resource_path, tempfile_path).and_return(true) expect(provider).to receive(:diff).at_least(:once).and_return(diff) - expect(provider).to receive(:managing_content?).at_least(:once).and_return(true) expect(provider).to receive(:checksum).with(tempfile_path).and_return(tempfile_sha256) - expect(provider).to receive(:checksum).with(resource_path).and_return(tempfile_sha256) + allow(provider).to receive(:managing_content?).and_return(true) + allow(provider).to receive(:checksum).with(resource_path).and_return(tempfile_sha256) + expect(resource).not_to receive(:checksum).with(tempfile_sha256) # do not mutate the new resource expect(provider.deployment_strategy).to receive(:deploy).with(tempfile_path, normalized_path) end context "when the file was created" do before { expect(provider).to receive(:needs_creating?).at_least(:once).and_return(true) } - it "does not backup the file and does not produce a diff for reporting" do + it "does not backup the file" do expect(provider).not_to receive(:do_backup) provider.send(:do_contents_changes) + end + + it "does not produce a diff for reporting" do + provider.send(:do_contents_changes) expect(resource.diff).to be_nil end + + it "renders the final checksum correctly for reporting" do + provider.send(:do_contents_changes) + expect(resource.state_for_resource_reporter[:checksum]).to eql(tempfile_sha256) + end end context "when the file was not created" do - before { expect(provider).to receive(:needs_creating?).at_least(:once).and_return(false) } - it "backs up the file and produces a diff for reporting" do + before do + allow(provider).to receive(:do_backup) # stub do_backup + expect(provider).to receive(:needs_creating?).at_least(:once).and_return(false) + end + + it "backs up the file" do expect(provider).to receive(:do_backup) provider.send(:do_contents_changes) + end + + it "produces a diff for reporting" do + provider.send(:do_contents_changes) expect(resource.diff).to eq(diff_for_reporting) end + + it "renders the final checksum correctly for reporting" do + provider.send(:do_contents_changes) + expect(resource.state_for_resource_reporter[:checksum]).to eql(tempfile_sha256) + end end end diff --git a/spec/support/shared/unit/user_and_client_shared.rb b/spec/support/shared/unit/user_and_client_shared.rb new file mode 100644 index 0000000000..bc5ffa07c2 --- /dev/null +++ b/spec/support/shared/unit/user_and_client_shared.rb @@ -0,0 +1,115 @@ +# +# Author:: Tyler Cloke (<tyler@chef.io>) +# 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. +# + +shared_examples_for "user or client create" do + + context "when server API V1 is valid on the Chef Server receiving the request" do + + it "creates a new object via the API" do + expect(rest_v1).to receive(:post).with(url, payload).and_return({}) + object.create + end + + it "creates a new object via the API with a public_key when it exists" do + object.public_key "some_public_key" + expect(rest_v1).to receive(:post).with(url, payload.merge({:public_key => "some_public_key"})).and_return({}) + object.create + end + + context "raise error when create_key and public_key are both set" do + + before do + object.public_key "key" + object.create_key true + end + + it "rasies the proper error" do + expect { object.create }.to raise_error(error) + end + end + + context "when create_key == true" do + before do + object.create_key true + end + + it "creates a new object via the API with create_key" do + expect(rest_v1).to receive(:post).with(url, payload.merge({:create_key => true})).and_return({}) + object.create + end + end + + context "when chef_key is returned by the server" do + let(:chef_key) { + { + "chef_key" => { + "public_key" => "some_public_key" + } + } + } + + it "puts the public key into the objectr returned by create" do + expect(rest_v1).to receive(:post).with(url, payload).and_return(payload.merge(chef_key)) + new_object = object.create + expect(new_object.public_key).to eq("some_public_key") + end + + context "when private_key is returned in chef_key" do + let(:chef_key) { + { + "chef_key" => { + "public_key" => "some_public_key", + "private_key" => "some_private_key" + } + } + } + + it "puts the private key into the object returned by create" do + expect(rest_v1).to receive(:post).with(url, payload).and_return(payload.merge(chef_key)) + new_object = object.create + expect(new_object.private_key).to eq("some_private_key") + end + end + end # when chef_key is returned by the server + + end # when server API V1 is valid on the Chef Server receiving the request + + context "when server API V1 is not valid on the Chef Server receiving the request" do + + context "when the server supports API V0" do + before do + allow(object).to receive(:server_client_api_version_intersection).and_return([0]) + allow(rest_v1).to receive(:post).and_raise(exception_406) + end + + it "creates a new object via the API" do + expect(rest_v0).to receive(:post).with(url, payload).and_return({}) + object.create + end + + it "creates a new object via the API with a public_key when it exists" do + object.public_key "some_public_key" + expect(rest_v0).to receive(:post).with(url, payload.merge({:public_key => "some_public_key"})).and_return({}) + object.create + end + + end # when the server supports API V0 + end # when server API V1 is not valid on the Chef Server receiving the request + +end # user or client create + diff --git a/spec/unit/api_client_spec.rb b/spec/unit/api_client_spec.rb index 7668e31f5a..a0e399b470 100644 --- a/spec/unit/api_client_spec.rb +++ b/spec/unit/api_client_spec.rb @@ -21,6 +21,11 @@ require 'spec_helper' require 'chef/api_client' require 'tempfile' +# DEPRECATION NOTE +# +# This code will be removed in Chef 13 in favor of the code in Chef::ApiClientV1, +# which will be moved to this namespace. New development should occur in +# Chef::ApiClientV1 until the time before Chef 13. describe Chef::ApiClient do before(:each) do @client = Chef::ApiClient.new @@ -123,10 +128,6 @@ describe Chef::ApiClient do it "does not include the private key if not present" do expect(@json).not_to include("private_key") end - - include_examples "to_json equalivent to Chef::JSONCompat.to_json" do - let(:jsonable) { @client } - end end describe "when deserializing from JSON (string) using ApiClient#from_json" do @@ -222,8 +223,8 @@ describe Chef::ApiClient do "validator" => true, "json_class" => "Chef::ApiClient" } - @http_client = double("Chef::REST mock") - allow(Chef::REST).to receive(:new).and_return(@http_client) + @http_client = double("Chef::ServerAPI mock") + allow(Chef::ServerAPI).to receive(:new).and_return(@http_client) expect(@http_client).to receive(:get).with("clients/black").and_return(client) @client = Chef::ApiClient.load(client['name']) end @@ -269,18 +270,13 @@ describe Chef::ApiClient do File.open(Chef::Config[:client_key], "r") {|f| f.read.chomp } end - it "has an HTTP client configured with default credentials" do - expect(@client.http_api).to be_a_kind_of(Chef::REST) - expect(@client.http_api.client_name).to eq("silent-bob") - expect(@client.http_api.signing_key.to_s).to eq(private_key_data) - end end describe "when requesting a new key" do before do @http_client = double("Chef::REST mock") - allow(Chef::REST).to receive(:new).and_return(@http_client) + allow(Chef::ServerAPI).to receive(:new).and_return(@http_client) end context "and the client does not exist on the server" do diff --git a/spec/unit/api_client_v1_spec.rb b/spec/unit/api_client_v1_spec.rb new file mode 100644 index 0000000000..17aba8c3af --- /dev/null +++ b/spec/unit/api_client_v1_spec.rb @@ -0,0 +1,457 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, 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/api_client_v1' +require 'tempfile' + +describe Chef::ApiClientV1 do + before(:each) do + @client = Chef::ApiClientV1.new + end + + it "has a name attribute" do + @client.name("ops_master") + expect(@client.name).to eq("ops_master") + end + + it "does not allow spaces in the name" do + expect { @client.name "ops master" }.to raise_error(ArgumentError) + end + + it "only allows string values for the name" do + expect { @client.name Hash.new }.to raise_error(ArgumentError) + end + + it "has an admin flag attribute" do + @client.admin(true) + expect(@client.admin).to be_truthy + end + + it "defaults to non-admin" do + expect(@client.admin).to be_falsey + end + + it "allows only boolean values for the admin flag" do + expect { @client.admin(false) }.not_to raise_error + expect { @client.admin(Hash.new) }.to raise_error(ArgumentError) + end + + it "has an create_key flag attribute" do + @client.create_key(true) + expect(@client.create_key).to be_truthy + end + + it "create_key defaults to false" do + expect(@client.create_key).to be_falsey + end + + it "allows only boolean values for the create_key flag" do + expect { @client.create_key(false) }.not_to raise_error + expect { @client.create_key(Hash.new) }.to raise_error(ArgumentError) + end + + it "has a 'validator' flag attribute" do + @client.validator(true) + expect(@client.validator).to be_truthy + end + + it "defaults to non-validator" do + expect(@client.validator).to be_falsey + end + + it "allows only boolean values for the 'validator' flag" do + expect { @client.validator(false) }.not_to raise_error + expect { @client.validator(Hash.new) }.to raise_error(ArgumentError) + end + + it "has a public key attribute" do + @client.public_key("super public") + expect(@client.public_key).to eq("super public") + end + + it "accepts only String values for the public key" do + expect { @client.public_key "" }.not_to raise_error + expect { @client.public_key Hash.new }.to raise_error(ArgumentError) + end + + + it "has a private key attribute" do + @client.private_key("super private") + expect(@client.private_key).to eq("super private") + end + + it "accepts only String values for the private key" do + expect { @client.private_key "" }.not_to raise_error + expect { @client.private_key Hash.new }.to raise_error(ArgumentError) + end + + describe "when serializing to JSON" do + before(:each) do + @client.name("black") + @client.public_key("crowes") + @json = @client.to_json + end + + it "serializes as a JSON object" do + expect(@json).to match(/^\{.+\}$/) + end + + it "includes the name value" do + expect(@json).to include(%q{"name":"black"}) + end + + it "includes the public key value" do + expect(@json).to include(%{"public_key":"crowes"}) + end + + it "includes the 'admin' flag" do + expect(@json).to include(%q{"admin":false}) + end + + it "includes the 'validator' flag" do + expect(@json).to include(%q{"validator":false}) + end + + it "includes the 'create_key' flag when present" do + @client.create_key(true) + @json = @client.to_json + expect(@json).to include(%q{"create_key":true}) + end + + it "includes the private key when present" do + @client.private_key("monkeypants") + expect(@client.to_json).to include(%q{"private_key":"monkeypants"}) + end + + it "does not include the private key if not present" do + expect(@json).not_to include("private_key") + end + + include_examples "to_json equivalent to Chef::JSONCompat.to_json" do + let(:jsonable) { @client } + end + end + + describe "when deserializing from JSON (string) using ApiClient#from_json" do + let(:client_string) do + "{\"name\":\"black\",\"public_key\":\"crowes\",\"private_key\":\"monkeypants\",\"admin\":true,\"validator\":true,\"create_key\":true}" + end + + let(:client) do + Chef::ApiClientV1.from_json(client_string) + end + + it "does not require a 'json_class' string" do + expect(Chef::JSONCompat.parse(client_string)["json_class"]).to eq(nil) + end + + it "should deserialize to a Chef::ApiClientV1 object" do + expect(client).to be_a_kind_of(Chef::ApiClientV1) + end + + it "preserves the name" do + expect(client.name).to eq("black") + end + + it "preserves the public key" do + expect(client.public_key).to eq("crowes") + end + + it "preserves the admin status" do + expect(client.admin).to be_truthy + end + + it "preserves the create_key status" do + expect(client.create_key).to be_truthy + end + + it "preserves the 'validator' status" do + expect(client.validator).to be_truthy + end + + it "includes the private key if present" do + expect(client.private_key).to eq("monkeypants") + end + end + + describe "when deserializing from JSON (hash) using ApiClientV1#from_json" do + let(:client_hash) do + { + "name" => "black", + "public_key" => "crowes", + "private_key" => "monkeypants", + "admin" => true, + "validator" => true, + "create_key" => true + } + end + + let(:client) do + Chef::ApiClientV1.from_json(Chef::JSONCompat.to_json(client_hash)) + end + + it "should deserialize to a Chef::ApiClientV1 object" do + expect(client).to be_a_kind_of(Chef::ApiClientV1) + end + + it "preserves the name" do + expect(client.name).to eq("black") + end + + it "preserves the public key" do + expect(client.public_key).to eq("crowes") + end + + it "preserves the admin status" do + expect(client.admin).to be_truthy + end + + it "preserves the create_key status" do + expect(client.create_key).to be_truthy + end + + it "preserves the 'validator' status" do + expect(client.validator).to be_truthy + end + + it "includes the private key if present" do + expect(client.private_key).to eq("monkeypants") + end + end + + describe "when loading from JSON" do + before do + end + + before(:each) do + client = { + "name" => "black", + "clientname" => "black", + "public_key" => "crowes", + "private_key" => "monkeypants", + "admin" => true, + "create_key" => true, + "validator" => true + } + + @http_client = double("Chef::ServerAPI mock") + allow(Chef::ServerAPI).to receive(:new).and_return(@http_client) + expect(@http_client).to receive(:get).with("clients/black").and_return(client) + @client = Chef::ApiClientV1.load(client['name']) + end + + it "should deserialize to a Chef::ApiClientV1 object" do + expect(@client).to be_a_kind_of(Chef::ApiClientV1) + end + + it "preserves the name" do + expect(@client.name).to eq("black") + end + + it "preserves the public key" do + expect(@client.public_key).to eq("crowes") + end + + it "preserves the admin status" do + expect(@client.admin).to be_a_kind_of(TrueClass) + end + + it "preserves the create_key status" do + expect(@client.create_key).to be_a_kind_of(TrueClass) + end + + it "preserves the 'validator' status" do + expect(@client.validator).to be_a_kind_of(TrueClass) + end + + it "includes the private key if present" do + expect(@client.private_key).to eq("monkeypants") + end + + end + + describe "with correctly configured API credentials" do + before do + Chef::Config[:node_name] = "silent-bob" + Chef::Config[:client_key] = File.expand_path('ssl/private_key.pem', CHEF_SPEC_DATA) + end + + after do + Chef::Config[:node_name] = nil + Chef::Config[:client_key] = nil + end + + let :private_key_data do + File.open(Chef::Config[:client_key], "r") {|f| f.read.chomp } + end + + end + + + describe "when requesting a new key" do + before do + @http_client = double("Chef::ServerAPI mock") + allow(Chef::ServerAPI).to receive(:new).and_return(@http_client) + end + + context "and the client does not exist on the server" do + before do + @a_404_response = Net::HTTPNotFound.new("404 not found and such", nil, nil) + @a_404_exception = Net::HTTPServerException.new("404 not found exception", @a_404_response) + + expect(@http_client).to receive(:get).with("clients/lost-my-key").and_raise(@a_404_exception) + end + + it "raises a 404 error" do + expect { Chef::ApiClientV1.reregister("lost-my-key") }.to raise_error(Net::HTTPServerException) + end + end + end + + describe "Versioned API Interactions" do + let(:response_406) { OpenStruct.new(:code => '406') } + let(:exception_406) { Net::HTTPServerException.new("406 Not Acceptable", response_406) } + let(:payload) { + { + :name => "some_name", + :validator => true, + :admin => true + } + } + + before do + @client = Chef::ApiClientV1.new + allow(@client).to receive(:chef_rest_v0).and_return(double('chef rest root v0 object')) + allow(@client).to receive(:chef_rest_v1).and_return(double('chef rest root v1 object')) + @client.name "some_name" + @client.validator true + @client.admin true + end + + describe "create" do + + # from spec/support/shared/unit/user_and_client_shared.rb + it_should_behave_like "user or client create" do + let(:object) { @client } + let(:error) { Chef::Exceptions::InvalidClientAttribute } + let(:rest_v0) { @client.chef_rest_v0 } + let(:rest_v1) { @client.chef_rest_v1 } + let(:url) { "clients" } + end + + context "when API V1 is not supported by the server" do + # from spec/support/shared/unit/api_versioning.rb + it_should_behave_like "version handling" do + let(:object) { @client } + let(:method) { :create } + let(:http_verb) { :post } + let(:rest_v1) { @client.chef_rest_v1 } + end + end + + end # create + + describe "update" do + context "when a valid client is defined" do + + shared_examples_for "client updating" do + it "updates the client" do + expect(rest). to receive(:put).with("clients/some_name", payload).and_return(payload) + @client.update + end + + context "when only the name field exists" do + + before do + # needed since there is no way to set to nil via code + @client.instance_variable_set(:@validator, nil) + @client.instance_variable_set(:@admin, nil) + end + + after do + @client.validator true + @client.admin true + end + + it "updates the client with only the name" do + expect(rest). to receive(:put).with("clients/some_name", {:name => "some_name"}).and_return({:name => "some_name"}) + @client.update + end + end + + end + + context "when API V1 is supported by the server" do + + it_should_behave_like "client updating" do + let(:rest) { @client.chef_rest_v1 } + end + + end # when API V1 is supported by the server + + context "when API V1 is not supported by the server" do + context "when no version is supported" do + # from spec/support/shared/unit/api_versioning.rb + it_should_behave_like "version handling" do + let(:object) { @client } + let(:method) { :create } + let(:http_verb) { :post } + let(:rest_v1) { @client.chef_rest_v1 } + end + end # when no version is supported + + context "when API V0 is supported" do + + before do + allow(@client.chef_rest_v1).to receive(:put).and_raise(exception_406) + allow(@client).to receive(:server_client_api_version_intersection).and_return([0]) + end + + it_should_behave_like "client updating" do + let(:rest) { @client.chef_rest_v0 } + end + + end + + end # when API V1 is not supported by the server + end # when a valid client is defined + end # update + + # DEPRECATION + # This can be removed after API V0 support is gone + describe "reregister" do + context "when server API V0 is valid on the Chef Server receiving the request" do + it "creates a new object via the API" do + expect(@client.chef_rest_v0).to receive(:put).with("clients/#{@client.name}", payload.merge({:private_key => true})).and_return({}) + @client.reregister + end + end # when server API V0 is valid on the Chef Server receiving the request + + context "when server API V0 is not supported by the Chef Server" do + # from spec/support/shared/unit/api_versioning.rb + it_should_behave_like "user and client reregister" do + let(:object) { @client } + let(:rest_v0) { @client.chef_rest_v0 } + end + end # when server API V0 is not supported by the Chef Server + end # reregister + + end +end diff --git a/spec/unit/application/client_spec.rb b/spec/unit/application/client_spec.rb index ea2ad473e5..64a6bcc9d2 100644 --- a/spec/unit/application/client_spec.rb +++ b/spec/unit/application/client_spec.rb @@ -60,7 +60,7 @@ describe Chef::Application::Client, "reconfigure" do context "when interval is given" do before do Chef::Config[:interval] = 600 - allow(Chef::Platform).to receive(:windows?).and_return(false) + allow(ChefConfig).to receive(:windows?).and_return(false) end it "should terminate with message" do @@ -77,7 +77,7 @@ Enable chef-client interval runs by setting `:client_fork = true` in your config context "when interval is given on windows" do before do Chef::Config[:interval] = 600 - allow(Chef::Platform).to receive(:windows?).and_return(true) + allow(ChefConfig).to receive(:windows?).and_return(true) end it "should not terminate" do @@ -131,6 +131,16 @@ Enable chef-client interval runs by setting `:client_fork = true` in your config end + describe "when --no-listen is set" do + + it "configures listen = false" do + app.config[:listen] = false + app.reconfigure + expect(Chef::Config[:listen]).to eq(false) + end + + end + describe "when the json_attribs configuration option is specified" do let(:json_attribs) { {"a" => "b"} } @@ -155,11 +165,6 @@ Enable chef-client interval runs by setting `:client_fork = true` in your config before do allow(Chef::Log).to receive(:warn) end - - it "emits a warning that audit mode is an experimental feature" do - expect(Chef::Log).to receive(:warn).with(/Audit mode is an experimental feature/) - app.reconfigure - end end shared_examples "unrecognized setting" do @@ -305,7 +310,7 @@ describe Chef::Application::Client, "run_application", :unix_only do allow(Chef::Daemon).to receive(:daemonize).and_return(true) end - it "should exit hard with exitstatus 3" do + it "should exit hard with exitstatus 3", :volatile do pid = fork do @app.run_application end @@ -320,9 +325,9 @@ describe Chef::Application::Client, "run_application", :unix_only do end expect(@pipe[0].gets).to eq("started\n") Process.kill("TERM", pid) - Process.wait - sleep 1 # Make sure we give the converging child process enough time to finish - expect(IO.select([@pipe[0]], nil, nil, 0)).not_to be_nil + Process.wait(pid) + # The timeout value needs to be large enough for the child process to finish + expect(IO.select([@pipe[0]], nil, nil, 15)).not_to be_nil expect(@pipe[0].gets).to eq("finished\n") end end diff --git a/spec/unit/application/solo_spec.rb b/spec/unit/application/solo_spec.rb index 1785ecfc86..7013bfa0bc 100644 --- a/spec/unit/application/solo_spec.rb +++ b/spec/unit/application/solo_spec.rb @@ -106,7 +106,8 @@ Enable chef-client interval runs by setting `:client_fork = true` in your config describe "when the recipe_url configuration option is specified" do let(:tarfile) { StringIO.new("remote_tarball_content") } let(:target_file) { StringIO.new } - + let(:shellout) { double(run_command: nil, error!: nil, stdout: '') } + before do Chef::Config[:cookbook_path] = "#{Dir.tmpdir}/chef-solo/cookbooks" Chef::Config[:recipe_url] = "http://junglist.gen.nz/recipes.tgz" @@ -117,7 +118,7 @@ Enable chef-client interval runs by setting `:client_fork = true` in your config allow(app).to receive(:open).with("http://junglist.gen.nz/recipes.tgz").and_yield(tarfile) allow(File).to receive(:open).with("#{Dir.tmpdir}/chef-solo/recipes.tgz", "wb").and_yield(target_file) - allow(Chef::Mixin::Command).to receive(:run_command).and_return(true) + allow(Mixlib::ShellOut).to receive(:new).and_return(shellout) end it "should create the recipes path based on the parent of the cookbook path" do @@ -136,7 +137,7 @@ Enable chef-client interval runs by setting `:client_fork = true` in your config end it "should untar the target file to the parent of the cookbook path" do - expect(Chef::Mixin::Command).to receive(:run_command).with({:command => "tar zxvf #{Dir.tmpdir}/chef-solo/recipes.tgz -C #{Dir.tmpdir}/chef-solo"}).and_return(true) + expect(Mixlib::ShellOut).to receive(:new).with("tar zxvf #{Dir.tmpdir}/chef-solo/recipes.tgz -C #{Dir.tmpdir}/chef-solo") app.reconfigure end end diff --git a/spec/unit/audit/audit_reporter_spec.rb b/spec/unit/audit/audit_reporter_spec.rb index 84d7ea82f0..46c2a96b4c 100644 --- a/spec/unit/audit/audit_reporter_spec.rb +++ b/spec/unit/audit/audit_reporter_spec.rb @@ -88,6 +88,29 @@ describe Chef::Audit::AuditReporter do reporter.run_completed(node) end + context "when audit phase failed" do + + let(:audit_error) { double("AuditError", :class => "Chef::Exceptions::AuditError", + :message => "Audit phase failed with error message: derpderpderp", + :backtrace => ["/path/recipe.rb:57", "/path/library.rb:106"]) } + + before do + reporter.instance_variable_set(:@audit_phase_error, audit_error) + end + + it "reports an error" do + reporter.run_completed(node) + expect(run_data).to have_key(:error) + expect(run_data).to have_key(:error) + expect(run_data[:error]).to eq <<-EOM.strip! +Chef::Exceptions::AuditError: Audit phase failed with error message: derpderpderp +/path/recipe.rb:57 +/path/library.rb:106 +EOM + end + + end + context "when unable to post to server" do let(:error) do @@ -203,7 +226,7 @@ describe Chef::Audit::AuditReporter do it "doesn't send reports" do expect(reporter).to receive(:auditing_enabled?).and_return(true) expect(reporter).to receive(:run_status).and_return(nil) - expect(Chef::Log).to receive(:debug).with("Run failed before audits were initialized, not sending audit report to server") + expect(Chef::Log).to receive(:debug).with("Run failed before audit mode was initialized, not sending audit report to server") reporter.run_completed(node) end @@ -215,9 +238,13 @@ describe Chef::Audit::AuditReporter do let(:audit_data) { Chef::Audit::AuditData.new(node.name, run_id) } let(:run_data) { audit_data.to_hash } - let(:error) { double("AuditError", :class => "Chef::Exception::AuditError", - :message => "Well that certainly didn't work", - :backtrace => ["line 0", "line 1", "line 2"]) } + let(:audit_error) { double("AuditError", :class => "Chef::Exceptions::AuditError", + :message => "Audit phase failed with error message: derpderpderp", + :backtrace => ["/path/recipe.rb:57", "/path/library.rb:106"]) } + + let(:run_error) { double("RunError", :class => "Chef::Exceptions::RunError", + :message => "This error shouldn't be reported.", + :backtrace => ["fix it", "fix it", "fix it"]) } before do allow(reporter).to receive(:auditing_enabled?).and_return(true) @@ -226,15 +253,32 @@ describe Chef::Audit::AuditReporter do allow(audit_data).to receive(:to_hash).and_return(run_data) end - it "adds the error information to the reported data" do - expect(rest).to receive(:create_url) - expect(rest).to receive(:post) - reporter.run_failed(error) - expect(run_data).to have_key(:error) - expect(run_data[:error]).to eq "Chef::Exception::AuditError: Well that certainly didn't work\n" + - "line 0\nline 1\nline 2" + context "when no prior exception is stored" do + it "reports no error" do + expect(rest).to receive(:create_url) + expect(rest).to receive(:post) + reporter.run_failed(run_error) + expect(run_data).to_not have_key(:error) + end end + context "when some prior exception is stored" do + before do + reporter.instance_variable_set(:@audit_phase_error, audit_error) + end + + it "reports the prior error" do + expect(rest).to receive(:create_url) + expect(rest).to receive(:post) + reporter.run_failed(run_error) + expect(run_data).to have_key(:error) + expect(run_data[:error]).to eq <<-EOM.strip! +Chef::Exceptions::AuditError: Audit phase failed with error message: derpderpderp +/path/recipe.rb:57 +/path/library.rb:106 +EOM + end + end end shared_context "audit data" do @@ -270,14 +314,14 @@ describe Chef::Audit::AuditReporter do it "notifies audit phase finished to debug log" do expect(Chef::Log).to receive(:debug).with(/Audit Reporter completed/) - reporter.audit_phase_complete + reporter.audit_phase_complete("Output from audit mode") end it "collects audit data" do ordered_control_groups.each do |_name, group| expect(audit_data).to receive(:add_control_group).with(group) end - reporter.audit_phase_complete + reporter.audit_phase_complete("Output from audit mode") end end @@ -288,14 +332,14 @@ describe Chef::Audit::AuditReporter do it "notifies audit phase failed to debug log" do expect(Chef::Log).to receive(:debug).with(/Audit Reporter failed/) - reporter.audit_phase_failed(error) + reporter.audit_phase_failed(error, "Output from audit mode") end it "collects audit data" do ordered_control_groups.each do |_name, group| expect(audit_data).to receive(:add_control_group).with(group) end - reporter.audit_phase_failed(error) + reporter.audit_phase_failed(error, "Output from audit mode") end end diff --git a/spec/unit/audit/logger_spec.rb b/spec/unit/audit/logger_spec.rb new file mode 100644 index 0000000000..9dd9ce2cd9 --- /dev/null +++ b/spec/unit/audit/logger_spec.rb @@ -0,0 +1,42 @@ +# +# 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::Audit::Logger do + + before(:each) do + Chef::Audit::Logger.instance_variable_set(:@buffer, nil) + end + + it 'calling puts creates @buffer and adds the message' do + Chef::Audit::Logger.puts("Output message") + expect(Chef::Audit::Logger.read_buffer).to eq("Output message\n") + end + + it 'calling puts multiple times adds to the message' do + Chef::Audit::Logger.puts("Output message") + Chef::Audit::Logger.puts("Output message") + Chef::Audit::Logger.puts("Output message") + expect(Chef::Audit::Logger.read_buffer).to eq("Output message\nOutput message\nOutput message\n") + end + + it 'calling it before @buffer is set returns an empty string' do + expect(Chef::Audit::Logger.read_buffer).to eq("") + end + +end diff --git a/spec/unit/audit/runner_spec.rb b/spec/unit/audit/runner_spec.rb index 0bd4c18388..1de024260f 100644 --- a/spec/unit/audit/runner_spec.rb +++ b/spec/unit/audit/runner_spec.rb @@ -68,8 +68,8 @@ describe Chef::Audit::Runner do in_sub_process do runner.send(:setup) - expect(RSpec.configuration.output_stream).to eq(log_location) - expect(RSpec.configuration.error_stream).to eq(log_location) + expect(RSpec.configuration.output_stream).to eq(Chef::Audit::Logger) + expect(RSpec.configuration.error_stream).to eq(Chef::Audit::Logger) expect(RSpec.configuration.formatters.size).to eq(2) expect(RSpec.configuration.formatters).to include(instance_of(Chef::Audit::AuditEventProxy)) diff --git a/spec/unit/chef_class_spec.rb b/spec/unit/chef_class_spec.rb new file mode 100644 index 0000000000..f1b877520c --- /dev/null +++ b/spec/unit/chef_class_spec.rb @@ -0,0 +1,110 @@ +# +# Author:: Lamont Granquist (<lamont@chef.io>) +# Copyright:: Copyright (c) 2015 Opscode, 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 class" do + let(:platform) { "debian" } + + let(:node) do + node = Chef::Node.new + node.automatic['platform'] = platform + node + end + + let(:run_context) do + Chef::RunContext.new(node, nil, nil) + end + + let(:resource_priority_map) do + double("Chef::Platform::ResourcePriorityMap") + end + + let(:provider_priority_map) do + double("Chef::Platform::ProviderPriorityMap") + end + + before do + Chef.set_run_context(run_context) + Chef.set_node(node) + Chef.set_resource_priority_map(resource_priority_map) + Chef.set_provider_priority_map(provider_priority_map) + end + + context "priority maps" do + context "#get_provider_priority_array" do + it "should use the current node to get the right priority_map" do + expect(provider_priority_map).to receive(:get_priority_array).with(node, :http_request).and_return("stuff") + expect(Chef.get_provider_priority_array(:http_request)).to eql("stuff") + end + end + context "#get_resource_priority_array" do + it "should use the current node to get the right priority_map" do + expect(resource_priority_map).to receive(:get_priority_array).with(node, :http_request).and_return("stuff") + expect(Chef.get_resource_priority_array(:http_request)).to eql("stuff") + end + end + context "#set_provider_priority_array" do + it "should delegate to the provider_priority_map" do + expect(provider_priority_map).to receive(:set_priority_array).with(:http_request, ["a", "b"], platform: "debian").and_return("stuff") + expect(Chef.set_provider_priority_array(:http_request, ["a", "b"], platform: "debian")).to eql("stuff") + end + end + context "#set_priority_map_for_resource" do + it "should delegate to the resource_priority_map" do + expect(resource_priority_map).to receive(:set_priority_array).with(:http_request, ["a", "b"], platform: "debian").and_return("stuff") + expect(Chef.set_resource_priority_array(:http_request, ["a", "b"], platform: "debian")).to eql("stuff") + end + end + end + + context "#run_context" do + it "should return the injected RunContext" do + expect(Chef.run_context).to eql(run_context) + end + end + + context "#node" do + it "should return the injected Node" do + expect(Chef.node).to eql(node) + end + end + + context '#event_handler' do + it 'adds a new handler' do + x = 1 + Chef.event_handler do + on :converge_start do + x = 2 + end + end + expect(Chef::Config[:event_handlers]).to_not be_empty + Chef::Config[:event_handlers].first.send(:converge_start) + expect(x).to eq(2) + end + + it 'raise error if unknown event type is passed' do + expect do + Chef.event_handler do + on :yolo do + end + end + end.to raise_error(Chef::Exceptions::InvalidEventType) + end + end +end diff --git a/spec/unit/chef_fs/file_pattern_spec.rb b/spec/unit/chef_fs/file_pattern_spec.rb index a9f06e8424..ed5f314605 100644 --- a/spec/unit/chef_fs/file_pattern_spec.rb +++ b/spec/unit/chef_fs/file_pattern_spec.rb @@ -157,7 +157,7 @@ describe Chef::ChefFS::FilePattern do end end - context 'with simple pattern "a\*\b"', :pending => (Chef::Platform.windows?) do + context 'with simple pattern "a\*\b"', :skip => (Chef::Platform.windows?) do let(:pattern) { Chef::ChefFS::FilePattern.new('a\*\b') } it 'match?' do expect(pattern.match?('a*b')).to be_truthy @@ -264,7 +264,7 @@ describe Chef::ChefFS::FilePattern do end end - context 'with star pattern "/abc/d[a-z][0-9]f/ghi"', :pending => (Chef::Platform.windows?) do + context 'with star pattern "/abc/d[a-z][0-9]f/ghi"', :skip => (Chef::Platform.windows?) do let(:pattern) { Chef::ChefFS::FilePattern.new('/abc/d[a-z][0-9]f/ghi') } it 'match?' do expect(pattern.match?('/abc/de1f/ghi')).to be_truthy @@ -352,11 +352,7 @@ describe Chef::ChefFS::FilePattern do expect(pattern.could_match_children?('/abc/def/ghi')).to be_truthy expect(pattern.could_match_children?('abc')).to be_falsey end - it 'could_match_children? /abc** returns false for /xyz' do - pending 'Make could_match_children? more rigorous' - # At the moment, we return false for this, but in the end it would be nice to return true: - expect(pattern.could_match_children?('/xyz')).to be_falsey - end + it 'exact_child_name_under' do expect(pattern.exact_child_name_under('/')).to eq(nil) expect(pattern.exact_child_name_under('/abc')).to eq(nil) @@ -440,14 +436,6 @@ describe Chef::ChefFS::FilePattern do expect(p('/.').exact_path).to eq('/') expect(p('/.').match?('/')).to be_truthy end - it 'handles dot by itself', :pending => "decide what to do with dot by itself" do - expect(p('.').normalized_pattern).to eq('.') - expect(p('.').exact_path).to eq('.') - expect(p('.').match?('.')).to be_truthy - expect(p('./').normalized_pattern).to eq('.') - expect(p('./').exact_path).to eq('.') - expect(p('./').match?('.')).to be_truthy - end it 'handles dotdot' do expect(p('abc/../def').normalized_pattern).to eq('def') expect(p('abc/../def').exact_path).to eq('def') diff --git a/spec/unit/chef_fs/path_util_spec.rb b/spec/unit/chef_fs/path_util_spec.rb new file mode 100644 index 0000000000..42eb126dfb --- /dev/null +++ b/spec/unit/chef_fs/path_util_spec.rb @@ -0,0 +1,108 @@ +# +# Author:: Kartik Null Cating-Subramanian (<ksubramanian@chef.io>) +# Copyright:: Copyright (c) 2015 Opscode, 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/chef_fs/path_utils' + +describe Chef::ChefFS::PathUtils do + context 'invoking join' do + it 'joins well-behaved distinct path elements' do + expect(Chef::ChefFS::PathUtils.join('a', 'b', 'c')).to eq('a/b/c') + end + + it 'strips extraneous slashes in the middle of paths' do + expect(Chef::ChefFS::PathUtils.join('a/', '/b', '/c/')).to eq('a/b/c') + expect(Chef::ChefFS::PathUtils.join('a/', '/b', '///c/')).to eq('a/b/c') + end + + it 'preserves the whether the first element was absolute or not' do + expect(Chef::ChefFS::PathUtils.join('/a/', '/b', 'c/')).to eq('/a/b/c') + expect(Chef::ChefFS::PathUtils.join('///a/', '/b', 'c/')).to eq('/a/b/c') + end + end + + context 'invoking is_absolute?' do + it 'confirms that paths starting with / are absolute' do + expect(Chef::ChefFS::PathUtils.is_absolute?('/foo/bar/baz')).to be true + expect(Chef::ChefFS::PathUtils.is_absolute?('/foo')).to be true + end + + it 'confirms that paths starting with // are absolute even though that looks like some windows network path' do + expect(Chef::ChefFS::PathUtils.is_absolute?('//foo/bar/baz')).to be true + end + + it 'confirms that root is indeed absolute' do + expect(Chef::ChefFS::PathUtils.is_absolute?('/')).to be true + end + + it 'confirms that paths starting without / are relative' do + expect(Chef::ChefFS::PathUtils.is_absolute?('foo/bar/baz')).to be false + expect(Chef::ChefFS::PathUtils.is_absolute?('a')).to be false + end + + it 'returns false for an empty path.' do + expect(Chef::ChefFS::PathUtils.is_absolute?('')).to be false + end + end + + context 'invoking realest_path' do + let(:good_path) { File.dirname(__FILE__) } + let(:parent_path) { File.dirname(good_path) } + + it 'handles paths with no wildcards or globs' do + expect(Chef::ChefFS::PathUtils.realest_path(good_path)).to eq(File.expand_path(good_path)) + end + + it 'handles paths with .. and .' do + expect(Chef::ChefFS::PathUtils.realest_path(good_path+'/../.')).to eq(File.expand_path(parent_path)) + end + + it 'handles paths with *' do + expect(Chef::ChefFS::PathUtils.realest_path(good_path + '/*/foo')).to eq(File.expand_path(good_path + '/*/foo')) + end + + it 'handles directories that do not exist' do + expect(Chef::ChefFS::PathUtils.realest_path(good_path + '/something/or/other')).to eq(File.expand_path(good_path + '/something/or/other')) + end + + it 'handles root correctly' do + if Chef::Platform.windows? + expect(Chef::ChefFS::PathUtils.realest_path('C:/')).to eq('C:/') + else + expect(Chef::ChefFS::PathUtils.realest_path('/')).to eq('/') + end + end + end + + context 'invoking descendant_path' do + it 'handles paths with various casing on windows' do + allow(Chef::ChefFS).to receive(:windows?) { true } + expect(Chef::ChefFS::PathUtils.descendant_path('C:/ab/b/c', 'C:/AB/B')).to eq('c') + expect(Chef::ChefFS::PathUtils.descendant_path('C:/ab/b/c', 'c:/ab/B')).to eq('c') + end + + it 'returns nil if the path does not have the given ancestor' do + expect(Chef::ChefFS::PathUtils.descendant_path('/D/E/F', '/A/B/C')).to be_nil + expect(Chef::ChefFS::PathUtils.descendant_path('/A/B/D', '/A/B/C')).to be_nil + end + + it 'returns blank if the ancestor equals the path' do + expect(Chef::ChefFS::PathUtils.descendant_path('/A/B/D', '/A/B/D')).to eq('') + end + end +end diff --git a/spec/unit/client_spec.rb b/spec/unit/client_spec.rb index 2ec32b32ac..8146774764 100644 --- a/spec/unit/client_spec.rb +++ b/spec/unit/client_spec.rb @@ -19,62 +19,31 @@ # require 'spec_helper' +require 'spec/support/shared/context/client' +require 'spec/support/shared/examples/client' require 'chef/run_context' require 'chef/rest' require 'rbconfig' -describe Chef::Client do +class FooError < RuntimeError +end - let(:hostname) { "hostname" } - let(:machinename) { "machinename.example.org" } - let(:fqdn) { "hostname.example.org" } - - let(:ohai_data) do - { :fqdn => fqdn, - :hostname => hostname, - :machinename => machinename, - :platform => 'example-platform', - :platform_version => 'example-platform-1.0', - :data => {} - } - end +describe Chef::Client do + include_context "client" - let(:ohai_system) do - ohai_system = double( "Ohai::System", - :all_plugins => true, - :data => ohai_data) - allow(ohai_system).to receive(:[]) do |key| - ohai_data[key] + context "when minimal ohai is configured" do + before do + Chef::Config[:minimal_ohai] = true end - ohai_system - end - let(:node) do - Chef::Node.new.tap do |n| - n.name(fqdn) - n.chef_environment("_default") + it "runs ohai with only the minimum required plugins" do + expected_filter = %w[fqdn machinename hostname platform platform_version os os_version] + expect(ohai_system).to receive(:all_plugins).with(expected_filter) + client.run_ohai end end - let(:json_attribs) { nil } - let(:client_opts) { {} } - - let(:client) do - Chef::Config[:event_loggers] = [] - Chef::Client.new(json_attribs, client_opts).tap do |c| - c.node = node - end - end - - before do - Chef::Log.logger = Logger.new(StringIO.new) - - # Node/Ohai data - #Chef::Config[:node_name] = fqdn - allow(Ohai::System).to receive(:new).and_return(ohai_system) - end - describe "authentication protocol selection" do after do Chef::Config[:authentication_protocol_version] = "1.0" @@ -101,7 +70,6 @@ describe Chef::Client do describe "configuring output formatters" do context "when no formatter has been configured" do - context "and STDOUT is a TTY" do before do allow(STDOUT).to receive(:tty?).and_return(true) @@ -187,135 +155,12 @@ describe Chef::Client do end describe "a full client run" do - shared_context "a client run" do - let(:http_node_load) { double("Chef::REST (node)") } - let(:http_cookbook_sync) { double("Chef::REST (cookbook sync)") } - let(:http_node_save) { double("Chef::REST (node save)") } - let(:runner) { double("Chef::Runner") } - let(:audit_runner) { instance_double("Chef::Audit::Runner", :failed? => false) } - - let(:api_client_exists?) { false } - - let(:stdout) { StringIO.new } - let(:stderr) { StringIO.new } - - let(:enable_fork) { false } - - def stub_for_register - # --Client.register - # Make sure Client#register thinks the client key doesn't - # exist, so it tries to register and create one. - allow(File).to receive(:exists?).and_call_original - expect(File).to receive(:exists?). - with(Chef::Config[:client_key]). - exactly(:once). - and_return(api_client_exists?) - - unless api_client_exists? - # Client.register will register with the validation client name. - expect_any_instance_of(Chef::ApiClient::Registration).to receive(:run) - end - end - - def stub_for_node_load - # Client.register will then turn around create another - # Chef::REST object, this time with the client key it got from the - # previous step. - expect(Chef::REST).to receive(:new). - with(Chef::Config[:chef_server_url], fqdn, Chef::Config[:client_key]). - exactly(:once). - and_return(http_node_load) - - # --Client#build_node - # looks up the node, which we will return, then later saves it. - expect(Chef::Node).to receive(:find_or_create).with(fqdn).and_return(node) - - # --ResourceReporter#node_load_completed - # gets a run id from the server for storing resource history - # (has its own tests, so stubbing it here.) - expect_any_instance_of(Chef::ResourceReporter).to receive(:node_load_completed) - end - - def stub_for_sync_cookbooks - # --Client#setup_run_context - # ---Client#sync_cookbooks -- downloads the list of cookbooks to sync - # - expect_any_instance_of(Chef::CookbookSynchronizer).to receive(:sync_cookbooks) - expect(Chef::REST).to receive(:new).with(Chef::Config[:chef_server_url]).and_return(http_cookbook_sync) - expect(http_cookbook_sync).to receive(:post). - with("environments/_default/cookbook_versions", {:run_list => []}). - and_return({}) - end - - def stub_for_converge - # --Client#converge - expect(Chef::Runner).to receive(:new).and_return(runner) - expect(runner).to receive(:converge).and_return(true) - end - - def stub_for_audit - # -- Client#run_audits - expect(Chef::Audit::Runner).to receive(:new).and_return(audit_runner) - expect(audit_runner).to receive(:run).and_return(true) - end - - def stub_for_node_save - allow(node).to receive(:data_for_save).and_return(node.for_json) - - # --Client#save_updated_node - expect(Chef::REST).to receive(:new).with(Chef::Config[:chef_server_url]).and_return(http_node_save) - expect(http_node_save).to receive(:put_rest).with("nodes/#{fqdn}", node.for_json).and_return(true) - end - - def stub_for_run - expect_any_instance_of(Chef::RunLock).to receive(:acquire) - expect_any_instance_of(Chef::RunLock).to receive(:save_pid) - expect_any_instance_of(Chef::RunLock).to receive(:release) - - # Post conditions: check that node has been filled in correctly - expect(client).to receive(:run_started) - expect(client).to receive(:run_completed_successfully) - - # --ResourceReporter#run_completed - # updates the server with the resource history - # (has its own tests, so stubbing it here.) - expect_any_instance_of(Chef::ResourceReporter).to receive(:run_completed) - # --AuditReporter#run_completed - # posts the audit data to server. - # (has its own tests, so stubbing it here.) - expect_any_instance_of(Chef::Audit::AuditReporter).to receive(:run_completed) - end - - before do - Chef::Config[:client_fork] = enable_fork - Chef::Config[:cache_path] = windows? ? 'C:\chef' : '/var/chef' - Chef::Config[:why_run] = false - Chef::Config[:audit_mode] = :enabled - - stub_const("Chef::Client::STDOUT_FD", stdout) - stub_const("Chef::Client::STDERR_FD", stderr) - - stub_for_register - stub_for_node_load - stub_for_sync_cookbooks - stub_for_converge - stub_for_audit - stub_for_node_save - stub_for_run - end - end - shared_examples_for "a successful client run" do include_context "a client run" + include_context "converge completed" + include_context "audit phase completed" - it "runs ohai, sets up authentication, loads node state, synchronizes policy, converges, and runs audits" do - # This is what we're testing. - client.run - - # fork is stubbed, so we can see the outcome of the run - expect(node.automatic_attrs[:platform]).to eq("example-platform") - expect(node.automatic_attrs[:platform_version]).to eq("example-platform-1.0") - end + include_examples "a completed run" end describe "when running chef-client without fork" do @@ -323,24 +168,19 @@ describe Chef::Client do end describe "when the client key already exists" do - let(:api_client_exists?) { true } - include_examples "a successful client run" + include_examples "a successful client run" do + let(:api_client_exists?) { true } + end end - describe "when an override run list is given" do - let(:client_opts) { {:override_runlist => "recipe[override_recipe]"} } - - it "should permit spaces in overriding run list" do + context "when an override run list is given" do + it "permits spaces in overriding run list" do Chef::Client.new(nil, :override_runlist => 'role[a], role[b]') end - describe "when running the client" do + describe "calling run" do include_examples "a successful client run" do - - before do - # Client will try to compile and run override_recipe - expect_any_instance_of(Chef::RunContext::CookbookCompiler).to receive(:compile) - end + let(:client_opts) { {:override_runlist => "recipe[override_recipe]"} } def stub_for_sync_cookbooks # --Client#setup_run_context @@ -357,13 +197,22 @@ describe Chef::Client do # Expect NO node save expect(node).not_to receive(:save) end + + before do + # Client will try to compile and run override_recipe + expect_any_instance_of(Chef::RunContext::CookbookCompiler).to receive(:compile) + end end end end describe "when a permanent run list is passed as an option" do - include_examples "a successful client run" do + it "sets the new run list on the node" do + client.run + expect(node.run_list).to eq(Chef::RunList.new(new_runlist)) + end + include_examples "a successful client run" do let(:new_runlist) { "recipe[new_run_list_recipe]" } let(:client_opts) { {:runlist => new_runlist} } @@ -383,168 +232,62 @@ describe Chef::Client do # do not create a fixture for this. expect_any_instance_of(Chef::RunContext::CookbookCompiler).to receive(:compile) end - - it "sets the new run list on the node" do - client.run - expect(node.run_list).to eq(Chef::RunList.new(new_runlist)) - end end end - describe "when converge fails" do - include_context "a client run" do - let(:e) { Exception.new } - def stub_for_converge - expect(Chef::Runner).to receive(:new).and_return(runner) - expect(runner).to receive(:converge).and_raise(e) - expect(Chef::Application).to receive(:debug_stacktrace).with an_instance_of(Chef::Exceptions::RunFailedWrappingError) - end - - def stub_for_node_save - expect(client).to_not receive(:save_updated_node) - end - - def stub_for_run - expect_any_instance_of(Chef::RunLock).to receive(:acquire) - expect_any_instance_of(Chef::RunLock).to receive(:save_pid) - expect_any_instance_of(Chef::RunLock).to receive(:release) - - # Post conditions: check that node has been filled in correctly - expect(client).to receive(:run_started) - expect(client).to receive(:run_failed) - - expect_any_instance_of(Chef::ResourceReporter).to receive(:run_failed) - expect_any_instance_of(Chef::Audit::AuditReporter).to receive(:run_failed) - end - end - - it "runs the audits and raises the error" do - expect{ client.run }.to raise_error(Chef::Exceptions::RunFailedWrappingError) do |error| - expect(error.wrapped_errors.size).to eq(1) - expect(error.wrapped_errors[0]).to eq(e) - end - end - end - - describe "when the audit phase fails" do - context "with an exception" do - include_context "a client run" do - let(:e) { Exception.new } - def stub_for_audit - expect(Chef::Audit::Runner).to receive(:new).and_return(audit_runner) - expect(audit_runner).to receive(:run).and_raise(e) - expect(Chef::Application).to receive(:debug_stacktrace).with an_instance_of(Chef::Exceptions::RunFailedWrappingError) - end - - def stub_for_run - expect_any_instance_of(Chef::RunLock).to receive(:acquire) - expect_any_instance_of(Chef::RunLock).to receive(:save_pid) - expect_any_instance_of(Chef::RunLock).to receive(:release) - - # Post conditions: check that node has been filled in correctly - expect(client).to receive(:run_started) - expect(client).to receive(:run_failed) - - expect_any_instance_of(Chef::ResourceReporter).to receive(:run_failed) - expect_any_instance_of(Chef::Audit::AuditReporter).to receive(:run_failed) - end - end - - it "should save the node after converge and raise exception" do - expect{ client.run }.to raise_error(Chef::Exceptions::RunFailedWrappingError) do |error| - expect(error.wrapped_errors.size).to eq(1) - expect(error.wrapped_errors[0]).to eq(e) + describe "when converge completes successfully" do + include_context "a client run" + include_context "converge completed" + context 'when audit mode is enabled' do + describe "when audit phase errors" do + include_context "audit phase failed with error" + include_examples "a completed run with audit failure" do + let(:run_errors) { [audit_error] } end end - end - - context "with failed audits" do - include_context "a client run" do - let(:audit_runner) do - instance_double("Chef::Audit::Runner", :run => true, :failed? => true, :num_failed => 1, :num_total => 1) - end - - def stub_for_audit - expect(Chef::Audit::Runner).to receive(:new).and_return(audit_runner) - expect(Chef::Application).to receive(:debug_stacktrace).with an_instance_of(Chef::Exceptions::RunFailedWrappingError) - end - - def stub_for_run - expect_any_instance_of(Chef::RunLock).to receive(:acquire) - expect_any_instance_of(Chef::RunLock).to receive(:save_pid) - expect_any_instance_of(Chef::RunLock).to receive(:release) - # Post conditions: check that node has been filled in correctly - expect(client).to receive(:run_started) - expect(client).to receive(:run_failed) - - expect_any_instance_of(Chef::ResourceReporter).to receive(:run_failed) - expect_any_instance_of(Chef::Audit::AuditReporter).to receive(:run_failed) - end + describe "when audit phase completed" do + include_context "audit phase completed" + include_examples "a completed run" end - it "should save the node after converge and raise exception" do - expect{ client.run }.to raise_error(Chef::Exceptions::RunFailedWrappingError) do |error| - expect(error.wrapped_errors.size).to eq(1) - expect(error.wrapped_errors[0]).to be_instance_of(Chef::Exceptions::AuditsFailed) + describe "when audit phase completed with failed controls" do + include_context "audit phase completed with failed controls" + include_examples "a completed run with audit failure" do + let(:run_errors) { [audit_error] } end end end end - describe "when why_run mode is enabled" do - include_context "a client run" do - - before do - Chef::Config[:why_run] = true - end - - def stub_for_audit - expect(Chef::Audit::Runner).to_not receive(:new) - end - - def stub_for_node_save - # This is how we should be mocking external calls - not letting it fall all the way through to the - # REST call - expect(node).to receive(:save) - end - - it "runs successfully without enabling the audit runner" do - client.run + describe "when converge errors" do + include_context "a client run" + include_context "converge failed" - # fork is stubbed, so we can see the outcome of the run - expect(node.automatic_attrs[:platform]).to eq("example-platform") - expect(node.automatic_attrs[:platform_version]).to eq("example-platform-1.0") + describe "when audit phase errors" do + include_context "audit phase failed with error" + include_examples "a failed run" do + let(:run_errors) { [converge_error, audit_error] } end end - end - - describe "when audits are disabled" do - include_context "a client run" do - - before do - Chef::Config[:audit_mode] = :disabled - end - def stub_for_audit - expect(Chef::Audit::Runner).to_not receive(:new) + describe "when audit phase completed" do + include_context "audit phase completed" + include_examples "a failed run" do + let(:run_errors) { [converge_error] } end + end - it "runs successfully without enabling the audit runner" do - client.run - - # fork is stubbed, so we can see the outcome of the run - expect(node.automatic_attrs[:platform]).to eq("example-platform") - expect(node.automatic_attrs[:platform_version]).to eq("example-platform-1.0") + describe "when audit phase completed with failed controls" do + include_context "audit phase completed with failed controls" + include_examples "a failed run" do + let(:run_errors) { [converge_error, audit_error] } end end end - end - describe "when handling run failures" do - it "should remove the run_lock on failure of #load_node" do @run_lock = double("Chef::RunLock", :acquire => true) allow(Chef::RunLock).to receive(:new).and_return(@run_lock) @@ -618,6 +361,7 @@ describe Chef::Client do # check pre-conditions. expect(node[:roles]).to be_nil expect(node[:recipes]).to be_nil + expect(node[:expanded_run_list]).to be_nil allow(client.policy_builder).to receive(:node).and_return(node) @@ -630,7 +374,10 @@ describe Chef::Client do expect(node[:roles]).to include("role_containing_cookbook1") expect(node[:recipes]).not_to be_nil expect(node[:recipes].length).to eq(1) - expect(node[:recipes]).to include("cookbook1") + expect(node[:recipes]).to include("cookbook1::default") + expect(node[:expanded_run_list]).not_to be_nil + expect(node[:expanded_run_list].length).to eq(1) + expect(node[:expanded_run_list]).to include("cookbook1::default") end it "should set the environment from the specified configuration value" do @@ -653,7 +400,7 @@ describe Chef::Client do describe "windows_admin_check" do context "platform is not windows" do before do - allow(Chef::Platform).to receive(:windows?).and_return(false) + allow(ChefConfig).to receive(:windows?).and_return(false) end it "shouldn't be called" do @@ -664,7 +411,7 @@ describe Chef::Client do context "platform is windows" do before do - allow(Chef::Platform).to receive(:windows?).and_return(true) + allow(ChefConfig).to receive(:windows?).and_return(true) end it "should be called" do @@ -713,6 +460,7 @@ describe Chef::Client do Chef::Config[:solo] = true Chef::Config[:cookbook_path] = ["/path/to/invalid/cookbook_path"] end + context "when any directory of cookbook_path contains no cookbook" do it "raises CookbookNotFound error" do expect do @@ -757,4 +505,35 @@ describe Chef::Client do end end + + describe "always attempt to run handlers" do + subject { client } + before do + # fail on the first thing in begin block + allow_any_instance_of(Chef::RunLock).to receive(:save_pid).and_raise(NoMethodError) + end + + context 'when audit mode is enabled' do + before do + Chef::Config[:audit_mode] = :enabled + end + it "should run exception handlers on early fail" do + expect(subject).to receive(:run_failed) + expect { subject.run }.to raise_error(Chef::Exceptions::RunFailedWrappingError) do |error| + expect(error.wrapped_errors.size).to eq 1 + expect(error.wrapped_errors).to include(NoMethodError) + end + end + end + + context 'when audit mode is disabled' do + before do + Chef::Config[:audit_mode] = :disabled + end + it "should run exception handlers on early fail" do + expect(subject).to receive(:run_failed) + expect { subject.run }.to raise_error(NoMethodError) + end + end + end end diff --git a/spec/unit/config_spec.rb b/spec/unit/config_spec.rb index 06178f7733..8d155c61ab 100644 --- a/spec/unit/config_spec.rb +++ b/spec/unit/config_spec.rb @@ -1,550 +1,31 @@ -# -# Author:: Adam Jacob (<adam@opscode.com>) -# Author:: Kyle Goodwin (<kgoodwin@primerevenue.com>) -# Copyright:: Copyright (c) 2008 Opscode, 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/exceptions' -require 'chef/util/path_helper' -describe Chef::Config do - describe "config attribute writer: chef_server_url" do - before do - Chef::Config.chef_server_url = "https://junglist.gen.nz" - end - - it "sets the server url" do - expect(Chef::Config.chef_server_url).to eq("https://junglist.gen.nz") - end - - context "when the url has a leading space" do - before do - Chef::Config.chef_server_url = " https://junglist.gen.nz" - end - - it "strips the space from the url when setting" do - expect(Chef::Config.chef_server_url).to eq("https://junglist.gen.nz") - end - - end - - context "when the url is a frozen string" do - before do - Chef::Config.chef_server_url = " https://junglist.gen.nz".freeze - end - - it "strips the space from the url when setting without raising an error" do - expect(Chef::Config.chef_server_url).to eq("https://junglist.gen.nz") - end - end - - end - - describe "when configuring formatters" do - # if TTY and not(force-logger) - # formatter = configured formatter or default formatter - # formatter goes to STDOUT/ERR - # if log file is writeable - # log level is configured level or info - # log location is file - # else - # log level is warn - # log location is STDERR - # end - # elsif not(TTY) and force formatter - # formatter = configured formatter or default formatter - # if log_location specified - # formatter goes to log_location - # else - # formatter goes to STDOUT/ERR - # end - # else - # formatter = "null" - # log_location = configured-value or defualt - # log_level = info or defualt - # end - # - it "has an empty list of formatters by default" do - expect(Chef::Config.formatters).to eq([]) - end +require 'chef/config' - it "configures a formatter with a short name" do - Chef::Config.add_formatter(:doc) - expect(Chef::Config.formatters).to eq([[:doc, nil]]) - end +RSpec.describe Chef::Config do - it "configures a formatter with a file output" do - Chef::Config.add_formatter(:doc, "/var/log/formatter.log") - expect(Chef::Config.formatters).to eq([[:doc, "/var/log/formatter.log"]]) + shared_examples_for "deprecated by ohai but not deprecated" do + it "does not emit a deprecation warning when set" do + expect(Chef::Log).to_not receive(:warn). + with(/Ohai::Config\[:#{option}\] is deprecated/) + Chef::Config[option] = value + expect(Chef::Config[option]).to eq(value) end - end - describe "class method: manage_secret_key" do - before do - allow(Chef::FileCache).to receive(:load).and_return(true) - allow(Chef::FileCache).to receive(:has_key?).with("chef_server_cookie_id").and_return(false) + describe ":log_level" do + include_examples "deprecated by ohai but not deprecated" do + let(:option) { :log_level } + let(:value) { :debug } end - - it "should generate and store a chef server cookie id" do - expect(Chef::FileCache).to receive(:store).with("chef_server_cookie_id", /\w{40}/).and_return(true) - Chef::Config.manage_secret_key - end - - describe "when the filecache has a chef server cookie id key" do - before do - allow(Chef::FileCache).to receive(:has_key?).with("chef_server_cookie_id").and_return(true) - end - - it "should not generate and store a chef server cookie id" do - expect(Chef::FileCache).not_to receive(:store).with("chef_server_cookie_id", /\w{40}/) - Chef::Config.manage_secret_key - end - end - end - [ false, true ].each do |is_windows| - - context "On #{is_windows ? 'Windows' : 'Unix'}" do - def to_platform(*args) - Chef::Config.platform_specific_path(*args) - end - - before :each do - allow(Chef::Platform).to receive(:windows?).and_return(is_windows) - end - - describe "class method: platform_specific_path" do - if is_windows - it "should return a windows path on windows systems" do - path = "/etc/chef/cookbooks" - allow(Chef::Config).to receive(:env).and_return({ 'SYSTEMDRIVE' => 'C:' }) - # match on a regex that looks for the base path with an optional - # system drive at the beginning (c:) - # system drive is not hardcoded b/c it can change and b/c it is not present on linux systems - expect(Chef::Config.platform_specific_path(path)).to eq("C:\\chef\\cookbooks") - end - else - it "should return given path on non-windows systems" do - path = "/etc/chef/cookbooks" - expect(Chef::Config.platform_specific_path(path)).to eq("/etc/chef/cookbooks") - end - end - end - - describe "default values" do - let :primary_cache_path do - if is_windows - "#{Chef::Config.env['SYSTEMDRIVE']}\\chef" - else - "/var/chef" - end - end - - let :secondary_cache_path do - if is_windows - "#{Chef::Config[:user_home]}\\.chef" - else - "#{Chef::Config[:user_home]}/.chef" - end - end - - before do - if is_windows - allow(Chef::Config).to receive(:env).and_return({ 'SYSTEMDRIVE' => 'C:' }) - Chef::Config[:user_home] = 'C:\Users\charlie' - else - Chef::Config[:user_home] = '/Users/charlie' - end - - allow(Chef::Config).to receive(:path_accessible?).and_return(false) - end - - describe "Chef::Config[:cache_path]" do - context "when /var/chef exists and is accessible" do - it "defaults to /var/chef" do - allow(Chef::Config).to receive(:path_accessible?).with(to_platform("/var/chef")).and_return(true) - expect(Chef::Config[:cache_path]).to eq(primary_cache_path) - end - end - - context "when /var/chef does not exist and /var is accessible" do - it "defaults to /var/chef" do - allow(File).to receive(:exists?).with(to_platform("/var/chef")).and_return(false) - allow(Chef::Config).to receive(:path_accessible?).with(to_platform("/var")).and_return(true) - expect(Chef::Config[:cache_path]).to eq(primary_cache_path) - end - end - - context "when /var/chef does not exist and /var is not accessible" do - it "defaults to $HOME/.chef" do - allow(File).to receive(:exists?).with(to_platform("/var/chef")).and_return(false) - allow(Chef::Config).to receive(:path_accessible?).with(to_platform("/var")).and_return(false) - expect(Chef::Config[:cache_path]).to eq(secondary_cache_path) - end - end - - context "when /var/chef exists and is not accessible" do - it "defaults to $HOME/.chef" do - allow(File).to receive(:exists?).with(to_platform("/var/chef")).and_return(true) - allow(File).to receive(:readable?).with(to_platform("/var/chef")).and_return(true) - allow(File).to receive(:writable?).with(to_platform("/var/chef")).and_return(false) - - expect(Chef::Config[:cache_path]).to eq(secondary_cache_path) - end - end - - context "when chef is running in local mode" do - before do - Chef::Config.local_mode = true - end - - context "and config_dir is /a/b/c" do - before do - Chef::Config.config_dir to_platform('/a/b/c') - end - - it "cache_path is /a/b/c/local-mode-cache" do - expect(Chef::Config.cache_path).to eq(to_platform('/a/b/c/local-mode-cache')) - end - end - - context "and config_dir is /a/b/c/" do - before do - Chef::Config.config_dir to_platform('/a/b/c/') - end - - it "cache_path is /a/b/c/local-mode-cache" do - expect(Chef::Config.cache_path).to eq(to_platform('/a/b/c/local-mode-cache')) - end - end - end - end - - it "Chef::Config[:file_backup_path] defaults to /var/chef/backup" do - allow(Chef::Config).to receive(:cache_path).and_return(primary_cache_path) - backup_path = is_windows ? "#{primary_cache_path}\\backup" : "#{primary_cache_path}/backup" - expect(Chef::Config[:file_backup_path]).to eq(backup_path) - end - - it "Chef::Config[:ssl_verify_mode] defaults to :verify_peer" do - expect(Chef::Config[:ssl_verify_mode]).to eq(:verify_peer) - end - - it "Chef::Config[:ssl_ca_path] defaults to nil" do - expect(Chef::Config[:ssl_ca_path]).to be_nil - end - - # TODO can this be removed? - if !is_windows - it "Chef::Config[:ssl_ca_file] defaults to nil" do - expect(Chef::Config[:ssl_ca_file]).to be_nil - end - end - - it "Chef::Config[:data_bag_path] defaults to /var/chef/data_bags" do - allow(Chef::Config).to receive(:cache_path).and_return(primary_cache_path) - data_bag_path = is_windows ? "#{primary_cache_path}\\data_bags" : "#{primary_cache_path}/data_bags" - expect(Chef::Config[:data_bag_path]).to eq(data_bag_path) - end - - it "Chef::Config[:environment_path] defaults to /var/chef/environments" do - allow(Chef::Config).to receive(:cache_path).and_return(primary_cache_path) - environment_path = is_windows ? "#{primary_cache_path}\\environments" : "#{primary_cache_path}/environments" - expect(Chef::Config[:environment_path]).to eq(environment_path) - end - - describe "setting the config dir" do - - context "when the config file is /etc/chef/client.rb" do - - before do - Chef::Config.config_file = to_platform("/etc/chef/client.rb") - end - - it "config_dir is /etc/chef" do - expect(Chef::Config.config_dir).to eq(to_platform("/etc/chef")) - end - - context "and chef is running in local mode" do - before do - Chef::Config.local_mode = true - end - - it "config_dir is /etc/chef" do - expect(Chef::Config.config_dir).to eq(to_platform("/etc/chef")) - end - end - - context "when config_dir is set to /other/config/dir/" do - before do - Chef::Config.config_dir = to_platform("/other/config/dir/") - end - - it "yields the explicit value" do - expect(Chef::Config.config_dir).to eq(to_platform("/other/config/dir/")) - end - end - - end - - context "when the user's home dir is /home/charlie/" do - before do - Chef::Config.user_home = to_platform("/home/charlie") - end - - it "config_dir is /home/charlie/.chef/" do - expect(Chef::Config.config_dir).to eq(Chef::Util::PathHelper.join(to_platform("/home/charlie/.chef"), '')) - end - - context "and chef is running in local mode" do - before do - Chef::Config.local_mode = true - end - - it "config_dir is /home/charlie/.chef/" do - expect(Chef::Config.config_dir).to eq(Chef::Util::PathHelper.join(to_platform("/home/charlie/.chef"), '')) - end - end - end - - end - - if is_windows - describe "finding the windows embedded dir" do - let(:default_config_location) { "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-11.6.0/lib/chef/config.rb" } - let(:alternate_install_location) { "c:/my/alternate/install/place/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-11.6.0/lib/chef/config.rb" } - let(:non_omnibus_location) { "c:/my/dev/stuff/lib/ruby/gems/1.9.1/gems/chef-11.6.0/lib/chef/config.rb" } - - let(:default_ca_file) { "c:/opscode/chef/embedded/ssl/certs/cacert.pem" } - - it "finds the embedded dir in the default location" do - allow(Chef::Config).to receive(:_this_file).and_return(default_config_location) - expect(Chef::Config.embedded_dir).to eq("c:/opscode/chef/embedded") - end - - it "finds the embedded dir in a custom install location" do - allow(Chef::Config).to receive(:_this_file).and_return(alternate_install_location) - expect(Chef::Config.embedded_dir).to eq("c:/my/alternate/install/place/chef/embedded") - end - - it "doesn't error when not in an omnibus install" do - allow(Chef::Config).to receive(:_this_file).and_return(non_omnibus_location) - expect(Chef::Config.embedded_dir).to be_nil - end - - it "sets the ssl_ca_cert path if the cert file is available" do - allow(Chef::Config).to receive(:_this_file).and_return(default_config_location) - allow(File).to receive(:exist?).with(default_ca_file).and_return(true) - expect(Chef::Config.ssl_ca_file).to eq(default_ca_file) - end - end - end - end - - describe "Chef::Config[:user_home]" do - it "should set when HOME is provided" do - expected = to_platform("/home/kitten") - allow(Chef::Config).to receive(:env).and_return({ 'HOME' => expected }) - expect(Chef::Config[:user_home]).to eq(expected) - end - - it "should be set when only USERPROFILE is provided" do - expected = to_platform("/users/kitten") - allow(Chef::Config).to receive(:env).and_return({ 'USERPROFILE' => expected }) - expect(Chef::Config[:user_home]).to eq(expected) - end - - it "falls back to the current working directory when HOME and USERPROFILE is not set" do - allow(Chef::Config).to receive(:env).and_return({}) - expect(Chef::Config[:user_home]).to eq(Dir.pwd) - end - end - - describe "Chef::Config[:encrypted_data_bag_secret]" do - let(:db_secret_default_path){ to_platform("/etc/chef/encrypted_data_bag_secret") } - - before do - allow(File).to receive(:exist?).with(db_secret_default_path).and_return(secret_exists) - end - - context "/etc/chef/encrypted_data_bag_secret exists" do - let(:secret_exists) { true } - it "sets the value to /etc/chef/encrypted_data_bag_secret" do - expect(Chef::Config[:encrypted_data_bag_secret]).to eq db_secret_default_path - end - end - - context "/etc/chef/encrypted_data_bag_secret does not exist" do - let(:secret_exists) { false } - it "sets the value to nil" do - expect(Chef::Config[:encrypted_data_bag_secret]).to be_nil - end - end - end - - describe "Chef::Config[:event_handlers]" do - it "sets a event_handlers to an empty array by default" do - expect(Chef::Config[:event_handlers]).to eq([]) - end - it "should be able to add custom handlers" do - o = Object.new - Chef::Config[:event_handlers] << o - expect(Chef::Config[:event_handlers]).to be_include(o) - end - end - - describe "Chef::Config[:user_valid_regex]" do - context "on a platform that is not Windows" do - it "allows one letter usernames" do - any_match = Chef::Config[:user_valid_regex].any? { |regex| regex.match('a') } - expect(any_match).to be_truthy - end - end - end - - describe "Chef::Config[:internal_locale]" do - let(:shell_out) do - double("Chef::Mixin::ShellOut double", :exitstatus => 0, :stdout => locales) - end - - let(:locales) { locale_array.join("\n") } - - before do - allow(Chef::Config).to receive(:shell_out_with_systems_locale!).with("locale -a").and_return(shell_out) - end - - shared_examples_for "a suitable locale" do - it "returns an English UTF-8 locale" do - expect(Chef::Log).to_not receive(:warn).with(/Please install an English UTF-8 locale for Chef to use/) - expect(Chef::Log).to_not receive(:debug).with(/Defaulting to locale en_US.UTF-8 on Windows/) - expect(Chef::Log).to_not receive(:debug).with(/No usable locale -a command found/) - expect(Chef::Config.guess_internal_locale).to eq expected_locale - end - end - - context "when the result includes 'C.UTF-8'" do - include_examples "a suitable locale" do - let(:locale_array) { [expected_locale, "en_US.UTF-8"] } - let(:expected_locale) { "C.UTF-8" } - end - end - - context "when the result includes 'en_US.UTF-8'" do - include_examples "a suitable locale" do - let(:locale_array) { ["en_CA.UTF-8", expected_locale, "en_NZ.UTF-8"] } - let(:expected_locale) { "en_US.UTF-8" } - end - end - - context "when the result includes 'en_US.utf8'" do - include_examples "a suitable locale" do - let(:locale_array) { ["en_CA.utf8", "en_US.utf8", "en_NZ.utf8"] } - let(:expected_locale) { "en_US.UTF-8" } - end - end - - context "when the result includes 'en.UTF-8'" do - include_examples "a suitable locale" do - let(:locale_array) { ["en.ISO8859-1", expected_locale] } - let(:expected_locale) { "en.UTF-8" } - end - end - - context "when the result includes 'en_*.UTF-8'" do - include_examples "a suitable locale" do - let(:locale_array) { [expected_locale, "en_CA.UTF-8", "en_GB.UTF-8"] } - let(:expected_locale) { "en_AU.UTF-8" } - end - end - - context "when the result includes 'en_*.utf8'" do - include_examples "a suitable locale" do - let(:locale_array) { ["en_AU.utf8", "en_CA.utf8", "en_GB.utf8"] } - let(:expected_locale) { "en_AU.UTF-8" } - end - end - - context "when the result does not include 'en_*.UTF-8'" do - let(:locale_array) { ["af_ZA", "af_ZA.ISO8859-1", "af_ZA.ISO8859-15", "af_ZA.UTF-8"] } - - it "should fall back to C locale" do - expect(Chef::Log).to receive(:warn).with("Please install an English UTF-8 locale for Chef to use, falling back to C locale and disabling UTF-8 support.") - expect(Chef::Config.guess_internal_locale).to eq 'C' - end - end - - context "on error" do - let(:locale_array) { [] } - - before do - allow(Chef::Config).to receive(:shell_out_with_systems_locale!).and_raise("THIS IS AN ERROR") - end - - it "should default to 'en_US.UTF-8'" do - if is_windows - expect(Chef::Log).to receive(:debug).with("Defaulting to locale en_US.UTF-8 on Windows, until it matters that we do something else.") - else - expect(Chef::Log).to receive(:debug).with("No usable locale -a command found, assuming you have en_US.UTF-8 installed.") - end - expect(Chef::Config.guess_internal_locale).to eq "en_US.UTF-8" - end - end - end + describe ":log_location" do + include_examples "deprecated by ohai but not deprecated" do + let(:option) { :log_location } + let(:value) { "path/to/log" } end end - describe "Treating deprecation warnings as errors" do - - context "when using our default RSpec configuration" do - - it "defaults to treating deprecation warnings as errors" do - expect(Chef::Config[:treat_deprecation_warnings_as_errors]).to be(true) - end - - it "sets CHEF_TREAT_DEPRECATION_WARNINGS_AS_ERRORS environment variable" do - expect(ENV['CHEF_TREAT_DEPRECATION_WARNINGS_AS_ERRORS']).to eq("1") - end - - it "treats deprecation warnings as errors in child processes when testing" do - # Doing a full integration test where we launch a child process is slow - # and liable to break for weird reasons (bundler env stuff, etc.), so - # we're just checking that the presence of the environment variable - # causes treat_deprecation_warnings_as_errors to be set to true after a - # config reset. - Chef::Config.reset - expect(Chef::Config[:treat_deprecation_warnings_as_errors]).to be(true) - end - - end - - context "outside of our test environment" do - - before do - ENV.delete('CHEF_TREAT_DEPRECATION_WARNINGS_AS_ERRORS') - Chef::Config.reset - end - - it "defaults to NOT treating deprecation warnings as errors" do - expect(Chef::Config[:treat_deprecation_warnings_as_errors]).to be(false) - end - end - - - end end diff --git a/spec/unit/cookbook/cookbook_version_loader_spec.rb b/spec/unit/cookbook/cookbook_version_loader_spec.rb index 2c4ad11787..23ffc21f7f 100644 --- a/spec/unit/cookbook/cookbook_version_loader_spec.rb +++ b/spec/unit/cookbook/cookbook_version_loader_spec.rb @@ -20,7 +20,7 @@ require 'spec_helper' describe Chef::Cookbook::CookbookVersionLoader do before do - allow(Chef::Platform).to receive(:windows?) { false } + allow(ChefConfig).to receive(:windows?) { false } end describe "loading a cookbook" do diff --git a/spec/unit/cookbook/file_vendor_spec.rb b/spec/unit/cookbook/file_vendor_spec.rb index 4fad7d5808..145541a63f 100644 --- a/spec/unit/cookbook/file_vendor_spec.rb +++ b/spec/unit/cookbook/file_vendor_spec.rb @@ -21,9 +21,6 @@ describe Chef::Cookbook::FileVendor do let(:file_vendor_class) { Class.new(described_class) } - # A manifest is a Hash of the format defined by Chef::CookbookVersion#manifest - let(:manifest) { {:cookbook_name => "bob"} } - context "when configured to fetch files over http" do let(:http) { double("Chef::REST") } @@ -40,19 +37,42 @@ describe Chef::Cookbook::FileVendor do expect(file_vendor_class.initialization_options).to eq(http) end - it "creates a RemoteFileVendor for a given manifest" do - file_vendor = file_vendor_class.create_from_manifest(manifest) - expect(file_vendor).to be_a_kind_of(Chef::Cookbook::RemoteFileVendor) - expect(file_vendor.rest).to eq(http) - expect(file_vendor.cookbook_name).to eq("bob") + context "with a manifest from a cookbook version" do + + # A manifest is a Hash of the format defined by Chef::CookbookVersion#manifest + let(:manifest) { {:cookbook_name => "bob", :name => "bob-1.2.3"} } + + it "creates a RemoteFileVendor for a given manifest" do + file_vendor = file_vendor_class.create_from_manifest(manifest) + expect(file_vendor).to be_a_kind_of(Chef::Cookbook::RemoteFileVendor) + expect(file_vendor.rest).to eq(http) + expect(file_vendor.cookbook_name).to eq("bob") + end + end + context "with a manifest from a cookbook artifact" do + + # A manifest is a Hash of the format defined by Chef::CookbookVersion#manifest + let(:manifest) { {:name => "bob"} } + + it "creates a RemoteFileVendor for a given manifest" do + file_vendor = file_vendor_class.create_from_manifest(manifest) + expect(file_vendor).to be_a_kind_of(Chef::Cookbook::RemoteFileVendor) + expect(file_vendor.rest).to eq(http) + expect(file_vendor.cookbook_name).to eq("bob") + end + + end end context "when configured to load files from disk" do let(:cookbook_path) { %w[/var/chef/cookbooks /var/chef/other_cookbooks] } + # A manifest is a Hash of the format defined by Chef::CookbookVersion#manifest + let(:manifest) { {:cookbook_name => "bob"} } + before do file_vendor_class.fetch_from_disk(cookbook_path) end diff --git a/spec/unit/cookbook/metadata_spec.rb b/spec/unit/cookbook/metadata_spec.rb index 760ae5dd2a..1b30286f51 100644 --- a/spec/unit/cookbook/metadata_spec.rb +++ b/spec/unit/cookbook/metadata_spec.rb @@ -30,7 +30,7 @@ describe Chef::Cookbook::Metadata do :maintainer_email, :license, :platforms, :dependencies, :recommendations, :suggestions, :conflicting, :providing, :replacing, :attributes, :groupings, :recipes, :version, - :source_url, :issues_url ] + :source_url, :issues_url, :privacy ] end it "does not depend on object identity for equality" do @@ -148,6 +148,10 @@ describe Chef::Cookbook::Metadata do it "has an empty issues_url string" do expect(metadata.issues_url).to eq('') end + + it "is not private" do + expect(metadata.privacy).to eq(false) + end end describe "validation" do @@ -198,7 +202,8 @@ describe Chef::Cookbook::Metadata do :long_description => "Much Longer\nSeriously", :version => "0.6.0", :source_url => "http://example.com", - :issues_url => "http://example.com/issues" + :issues_url => "http://example.com/issues", + :privacy => true } params.sort { |a,b| a.to_s <=> b.to_s }.each do |field, field_value| describe field do @@ -304,6 +309,21 @@ describe Chef::Cookbook::Metadata do end end end + + it "strips out self-dependencies", :chef_lt_13_only do + metadata.name('foo') + expect(Chef::Log).to receive(:warn).with( + "Ignoring self-dependency in cookbook foo, please remove it (in the future this will be fatal)." + ) + metadata.depends('foo') + expect(metadata.dependencies).to eql({}) + end + + it "errors on self-dependencies", :chef_gte_13_only do + metadata.name('foo') + expect { metadata.depends('foo') }.to raise_error + # FIXME: add the error type + end end describe "attribute groupings" do @@ -345,7 +365,8 @@ describe Chef::Cookbook::Metadata do "recipes" => [ "mysql::server", "mysql::master" ], "default" => [ ], "source_url" => "http://example.com", - "issues_url" => "http://example.com/issues" + "issues_url" => "http://example.com/issues", + "privacy" => true } expect(metadata.attribute("/db/mysql/databases", attrs)).to eq(attrs) end @@ -386,6 +407,18 @@ describe Chef::Cookbook::Metadata do }.to raise_error(ArgumentError) end + it "should not accept anything but true or false for the privacy flag" do + expect { + metadata.attribute("db/mysql/databases", :privacy => true) + }.not_to raise_error + expect { + metadata.attribute("db/mysql/databases", :privacy => false) + }.not_to raise_error + expect { + metadata.attribute("db/mysql/databases", :privacy => 'true') + }.to raise_error(ArgumentError) + end + it "should not accept anything but an array of strings for choice" do expect { metadata.attribute("db/mysql/databases", :choice => ['dedicated', 'shared']) @@ -684,6 +717,7 @@ describe Chef::Cookbook::Metadata do version source_url issues_url + privacy }.each do |t| it "should include '#{t}'" do expect(deserialized_metadata[t]).to eq(metadata.send(t.to_sym)) @@ -719,6 +753,7 @@ describe Chef::Cookbook::Metadata do version source_url issues_url + privacy }.each do |t| it "should match '#{t}'" do expect(deserialized_metadata.send(t.to_sym)).to eq(metadata.send(t.to_sym)) diff --git a/spec/unit/cookbook/syntax_check_spec.rb b/spec/unit/cookbook/syntax_check_spec.rb index 471fc01831..764829c387 100644 --- a/spec/unit/cookbook/syntax_check_spec.rb +++ b/spec/unit/cookbook/syntax_check_spec.rb @@ -21,7 +21,7 @@ require "chef/cookbook/syntax_check" describe Chef::Cookbook::SyntaxCheck do before do - allow(Chef::Platform).to receive(:windows?) { false } + allow(ChefConfig).to receive(:windows?) { false } end let(:cookbook_path) { File.join(CHEF_SPEC_DATA, 'cookbooks', 'openldap') } @@ -53,6 +53,7 @@ describe Chef::Cookbook::SyntaxCheck do @ruby_files = @attr_files + @libr_files + @defn_files + @recipes + [File.join(cookbook_path, "metadata.rb")] basenames = %w{ helpers_via_partial_test.erb helper_test.erb + helpers.erb openldap_stuff.conf.erb openldap_variable_stuff.conf.erb test.erb diff --git a/spec/unit/cookbook_loader_spec.rb b/spec/unit/cookbook_loader_spec.rb index 45a985bafd..b1384bffe7 100644 --- a/spec/unit/cookbook_loader_spec.rb +++ b/spec/unit/cookbook_loader_spec.rb @@ -20,7 +20,7 @@ require 'spec_helper' describe Chef::CookbookLoader do before do - allow(Chef::Platform).to receive(:windows?) {false} + allow(ChefConfig).to receive(:windows?) {false} end let(:repo_paths) do [ diff --git a/spec/unit/cookbook_manifest_spec.rb b/spec/unit/cookbook_manifest_spec.rb index 938f72c743..f985942e09 100644 --- a/spec/unit/cookbook_manifest_spec.rb +++ b/spec/unit/cookbook_manifest_spec.rb @@ -24,6 +24,8 @@ describe Chef::CookbookManifest do let(:version) { "1.2.3" } + let(:identifier) { "9e10455ce2b4a4e29424b7064b1d67a1a25c9d3b" } + let(:metadata) do Chef::Cookbook::Metadata.new.tap do |m| m.version(version) @@ -35,6 +37,7 @@ describe Chef::CookbookManifest do let(:cookbook_version) do Chef::CookbookVersion.new("tatft", cookbook_root).tap do |c| c.metadata = metadata + c.identifier = identifier end end @@ -212,12 +215,26 @@ describe Chef::CookbookManifest do let(:policy_mode) { true } + let(:cookbook_manifest_hash) { cookbook_manifest.to_hash } + + it "sets the identifier in the manifest data" do + expect(cookbook_manifest_hash["identifier"]).to eq("9e10455ce2b4a4e29424b7064b1d67a1a25c9d3b") + end + + it "sets the name to just the name" do + expect(cookbook_manifest_hash["name"]).to eq("tatft") + end + + it "does not set a 'cookbook_name' field" do + expect(cookbook_manifest_hash).to_not have_key("cookbook_name") + end + it "gives the save URL" do - expect(cookbook_manifest.save_url).to eq("cookbook_artifacts/tatft/1.2.3") + expect(cookbook_manifest.save_url).to eq("cookbook_artifacts/tatft/9e10455ce2b4a4e29424b7064b1d67a1a25c9d3b") end it "gives the force save URL" do - expect(cookbook_manifest.force_save_url).to eq("cookbook_artifacts/tatft/1.2.3?force=true") + expect(cookbook_manifest.force_save_url).to eq("cookbook_artifacts/tatft/9e10455ce2b4a4e29424b7064b1d67a1a25c9d3b?force=true") end end diff --git a/spec/unit/cookbook_site_streaming_uploader_spec.rb b/spec/unit/cookbook_site_streaming_uploader_spec.rb index ef0f649163..0041a142dc 100644 --- a/spec/unit/cookbook_site_streaming_uploader_spec.rb +++ b/spec/unit/cookbook_site_streaming_uploader_spec.rb @@ -121,27 +121,6 @@ describe Chef::CookbookSiteStreamingUploader do }) end - describe "http verify mode" do - before do - @uri = "https://cookbooks.dummy.com/api/v1/cookbooks" - uri_info = URI.parse(@uri) - @http = Net::HTTP.new(uri_info.host, uri_info.port) - expect(Net::HTTP).to receive(:new).with(uri_info.host, uri_info.port).and_return(@http) - end - - it "should be VERIFY_NONE when ssl_verify_mode is :verify_none" do - Chef::Config[:ssl_verify_mode] = :verify_none - Chef::CookbookSiteStreamingUploader.make_request(:post, @uri, 'bill', @secret_filename) - expect(@http.verify_mode).to eq(OpenSSL::SSL::VERIFY_NONE) - end - - it "should be VERIFY_PEER when ssl_verify_mode is :verify_peer" do - Chef::Config[:ssl_verify_mode] = :verify_peer - Chef::CookbookSiteStreamingUploader.make_request(:post, @uri, 'bill', @secret_filename) - expect(@http.verify_mode).to eq(OpenSSL::SSL::VERIFY_PEER) - end - end - end # make_request describe "StreamPart" do diff --git a/spec/unit/cookbook_spec.rb b/spec/unit/cookbook_spec.rb index 7b3cda2af1..f36b031309 100644 --- a/spec/unit/cookbook_spec.rb +++ b/spec/unit/cookbook_spec.rb @@ -59,15 +59,6 @@ describe Chef::CookbookVersion do expect(@cookbook.fully_qualified_recipe_names.include?("openldap::three")).to eq(true) end - it "should find a preferred file" do - skip - end - - it "should not return an unchanged preferred file" do - pending - expect(@cookbook.preferred_filename(@node, :files, 'a-filename', 'the-checksum')).to be_nil - end - it "should raise an ArgumentException if you try to load a bad recipe name" do expect { @cookbook.load_recipe("doesnt_exist", @node) }.to raise_error(ArgumentError) end diff --git a/spec/unit/cookbook_uploader_spec.rb b/spec/unit/cookbook_uploader_spec.rb index 152e5373f0..76727c18e2 100644 --- a/spec/unit/cookbook_uploader_spec.rb +++ b/spec/unit/cookbook_uploader_spec.rb @@ -25,11 +25,17 @@ describe Chef::CookbookUploader do let(:cookbook_loader) do loader = Chef::CookbookLoader.new(File.join(CHEF_SPEC_DATA, "cookbooks")) loader.load_cookbooks + loader.cookbooks_by_name["apache2"].identifier = apache2_identifier + loader.cookbooks_by_name["java"].identifier = java_identifier loader end + let(:apache2_identifier) { "6644e6cb2ade90b8aff2ebb44728958fbc939ebf" } + let(:apache2_cookbook) { cookbook_loader.cookbooks_by_name["apache2"] } + let(:java_identifier) { "edd40c30c4e0ebb3658abde4620597597d2e9c17" } + let(:java_cookbook) { cookbook_loader.cookbooks_by_name["java"] } let(:cookbooks_to_upload) { [apache2_cookbook, java_cookbook] } @@ -175,7 +181,7 @@ describe Chef::CookbookUploader do let(:policy_mode) { true } def expected_save_url(cookbook) - "cookbook_artifacts/#{cookbook.name}/#{cookbook.version}" + "cookbook_artifacts/#{cookbook.name}/#{cookbook.identifier}" end it "uploads all files in a sandbox transaction, then creates cookbooks on the server using cookbook_artifacts API" do diff --git a/spec/unit/cookbook_version_spec.rb b/spec/unit/cookbook_version_spec.rb index 440dd9da6c..2bccddcaec 100644 --- a/spec/unit/cookbook_version_spec.rb +++ b/spec/unit/cookbook_version_spec.rb @@ -306,26 +306,6 @@ describe Chef::CookbookVersion do subject(:cbv) { Chef::CookbookVersion.new("version validation", '/tmp/blah') } - describe "HTTP Resource behaviors", pending: "will be deprected when CookbookManifest API is stablized" do - - it "errors on #save_url" do - expect { cbv.save_url }.to raise_error(Chef::Exceptions::DeprecatedFeatureError) - end - - it "errors on #force_save_url" do - expect { cbv.force_save_url }.to raise_error(Chef::Exceptions::DeprecatedFeatureError) - end - - it "errors on #to_hash" do - expect { cbv.to_hash }.to raise_error(Chef::Exceptions::DeprecatedFeatureError) - end - - it "errors on #to_json" do - expect { cbv.to_json }.to raise_error(Chef::Exceptions::DeprecatedFeatureError) - end - - end - it "errors on #status and #status=" do expect { cbv.status = :wat }.to raise_error(Chef::Exceptions::DeprecatedFeatureError) expect { cbv.status }.to raise_error(Chef::Exceptions::DeprecatedFeatureError) @@ -356,7 +336,7 @@ describe Chef::CookbookVersion do end - include_examples "to_json equalivent to Chef::JSONCompat.to_json" do + include_examples "to_json equivalent to Chef::JSONCompat.to_json" do let(:jsonable) { Chef::CookbookVersion.new("tatft", '/tmp/blah') } end diff --git a/spec/unit/data_bag_item_spec.rb b/spec/unit/data_bag_item_spec.rb index 4348252388..497817ecf1 100644 --- a/spec/unit/data_bag_item_spec.rb +++ b/spec/unit/data_bag_item_spec.rb @@ -193,7 +193,7 @@ describe Chef::DataBagItem do expect(deserial["snooze"]).to eq({ "finally" => "world_will" }) end - include_examples "to_json equalivent to Chef::JSONCompat.to_json" do + include_examples "to_json equivalent to Chef::JSONCompat.to_json" do let(:jsonable) { data_bag_item } end end diff --git a/spec/unit/data_bag_spec.rb b/spec/unit/data_bag_spec.rb index f6db1e222a..13b835d120 100644 --- a/spec/unit/data_bag_spec.rb +++ b/spec/unit/data_bag_spec.rb @@ -22,7 +22,7 @@ require 'chef/data_bag' describe Chef::DataBag do before(:each) do @data_bag = Chef::DataBag.new - allow(Chef::Platform)::to receive(:windows?) { false } + allow(ChefConfig).to receive(:windows?) { false } end describe "initialize" do @@ -73,7 +73,7 @@ describe Chef::DataBag do expect(@deserial.send(t.to_sym)).to eq(@data_bag.send(t.to_sym)) end - include_examples "to_json equalivent to Chef::JSONCompat.to_json" do + include_examples "to_json equivalent to Chef::JSONCompat.to_json" do let(:jsonable) { @data_bag } end end diff --git a/spec/unit/deprecation_spec.rb b/spec/unit/deprecation_spec.rb index f824cb7c76..674de5ec1d 100644 --- a/spec/unit/deprecation_spec.rb +++ b/spec/unit/deprecation_spec.rb @@ -65,19 +65,16 @@ describe Chef::Deprecation do end context 'deprecation warning messages' do - before(:each) do - @warning_output = [ ] - allow(Chef::Log).to receive(:warn) { |msg| @warning_output << msg } - end + RSpec::Matchers.define_negated_matcher :a_non_empty_array, :be_empty it 'should be enabled for deprecated methods' do + expect(Chef::Log).to receive(:warn).with(a_non_empty_array) TestClass.new.deprecated_method(10) - expect(@warning_output).not_to be_empty end it 'should contain stack trace' do + expect(Chef::Log).to receive(:warn).with(a_string_including(".rb")) TestClass.new.deprecated_method(10) - expect(@warning_output.join("").include?(".rb")).to be_truthy end end @@ -95,4 +92,59 @@ describe Chef::Deprecation do expect { test_instance.deprecated_method(10) }.to raise_error(Chef::Exceptions::DeprecatedFeatureError) end + context "When a class has deprecated_attr, _reader and _writer" do + before(:context) do + class DeprecatedAttrTest + extend Chef::Mixin::Deprecation + def initialize + @a = @r = @w = 1 + end + deprecated_attr :a, "a" + deprecated_attr_reader :r, "r" + deprecated_attr_writer :w, "w" + end + end + + it "The deprecated_attr emits warnings" do + test = DeprecatedAttrTest.new + expect { test.a = 10 }.to raise_error(Chef::Exceptions::DeprecatedFeatureError) + expect { test.a }.to raise_error(Chef::Exceptions::DeprecatedFeatureError) + end + + it "The deprecated_attr_writer emits warnings, and does not create a reader" do + test = DeprecatedAttrTest.new + expect { test.w = 10 }.to raise_error(Chef::Exceptions::DeprecatedFeatureError) + expect { test.w }.to raise_error(NoMethodError) + end + + it "The deprecated_attr_reader emits warnings, and does not create a writer" do + test = DeprecatedAttrTest.new + expect { test.r = 10 }.to raise_error(NoMethodError) + expect { test.r }.to raise_error(Chef::Exceptions::DeprecatedFeatureError) + end + + context "With deprecation warnings not throwing exceptions" do + before do + Chef::Config[:treat_deprecation_warnings_as_errors] = false + end + + it "The deprecated_attr can be written to and read from" do + test = DeprecatedAttrTest.new + test.a = 10 + expect(test.a).to eq 10 + end + + it "The deprecated_attr_reader can be read from" do + test = DeprecatedAttrTest.new + expect(test.r).to eq 1 + end + + it "The deprecated_attr_writer can be written to" do + test = DeprecatedAttrTest.new + test.w = 10 + expect(test.instance_eval { @w }).to eq 10 + end + end + end + end diff --git a/spec/unit/dsl/reboot_pending_spec.rb b/spec/unit/dsl/reboot_pending_spec.rb index 0f2288740f..a55f91d5e6 100644 --- a/spec/unit/dsl/reboot_pending_spec.rb +++ b/spec/unit/dsl/reboot_pending_spec.rb @@ -46,7 +46,7 @@ describe Chef::DSL::RebootPending do end it 'should return true if key "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootRequired" exists' do - allow(recipe).to receive(:registry_key_exists?).with('HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootRequired').and_return(true) + allow(recipe).to receive(:registry_key_exists?).with('HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending').and_return(true) expect(recipe.reboot_pending?).to be_truthy end diff --git a/spec/unit/dsl/resources_spec.rb b/spec/unit/dsl/resources_spec.rb new file mode 100644 index 0000000000..581c835290 --- /dev/null +++ b/spec/unit/dsl/resources_spec.rb @@ -0,0 +1,85 @@ +# +# Author:: Noah Kantrowitz (<noah@coderanger.net>) +# Copyright:: Copyright (c) 2015 Noah Kantrowitz +# 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/dsl/resources' + +describe Chef::DSL::Resources do + let(:declared_resources) { [] } + let(:test_class) do + r = declared_resources + Class.new do + include Chef::DSL::Resources + define_method(:declare_resource) do |dsl_name, name, _created_at, &_block| + r << [dsl_name, name] + end + end + end + subject { declared_resources } + after do + # Always clean up after ourselves. + described_class.remove_resource_dsl(:test_resource) + end + + context 'with a resource added' do + before do + Chef::DSL::Resources.add_resource_dsl(:test_resource) + test_class.new.instance_eval do + test_resource 'test_name' do + end + end + end + it { is_expected.to eq [[:test_resource, 'test_name']]} + end + + context 'with no resource added' do + subject do + test_class.new.instance_eval do + test_resource 'test_name' do + end + end + end + + it { expect { subject }.to raise_error NoMethodError } + end + + context 'with a resource added and removed' do + before do + Chef::DSL::Resources.add_resource_dsl(:test_resource) + Chef::DSL::Resources.remove_resource_dsl(:test_resource) + end + subject do + test_class.new.instance_eval do + test_resource 'test_name' do + end + end + end + + it { expect { subject }.to raise_error NoMethodError } + end + + context 'with a nameless resource' do + before do + Chef::DSL::Resources.add_resource_dsl(:test_resource) + test_class.new.instance_eval do + test_resource { } + end + end + it { is_expected.to eq [[:test_resource, nil]]} + end +end diff --git a/spec/unit/environment_spec.rb b/spec/unit/environment_spec.rb index ee3b8b21e1..64617e0888 100644 --- a/spec/unit/environment_spec.rb +++ b/spec/unit/environment_spec.rb @@ -208,7 +208,7 @@ describe Chef::Environment do expect(@json).to match(/"chef_type":"environment"/) end - include_examples "to_json equalivent to Chef::JSONCompat.to_json" do + include_examples "to_json equivalent to Chef::JSONCompat.to_json" do let(:jsonable) { @environment } end end diff --git a/spec/unit/event_dispatch/dispatcher_spec.rb b/spec/unit/event_dispatch/dispatcher_spec.rb new file mode 100644 index 0000000000..1014feea89 --- /dev/null +++ b/spec/unit/event_dispatch/dispatcher_spec.rb @@ -0,0 +1,80 @@ +# +# Author:: Daniel DeLeo (<dan@chef.io>) +# +# 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/event_dispatch/dispatcher' + +describe Chef::EventDispatch::Dispatcher do + + subject(:dispatcher) { Chef::EventDispatch::Dispatcher.new } + + let(:event_sink) { instance_double("Chef::EventDispatch::Base") } + + it "has no subscribers by default" do + expect(dispatcher.subscribers).to be_empty + end + + context "when an event sink is registered" do + + before do + dispatcher.register(event_sink) + end + + it "it has the event sink as a subscriber" do + expect(dispatcher.subscribers.size).to eq(1) + expect(dispatcher.subscribers.first).to eq(event_sink) + end + + it "forwards events to the subscribed event sink" do + # the events all have different arity and such so we just hit a few different events: + + expect(event_sink).to receive(:run_start).with("12.4.0") + dispatcher.run_start("12.4.0") + + cookbook_version = double("cookbook_version") + expect(event_sink).to receive(:synchronized_cookbook).with("apache2", cookbook_version) + dispatcher.synchronized_cookbook("apache2", cookbook_version) + + exception = StandardError.new("foo") + expect(event_sink).to receive(:recipe_file_load_failed).with("/path/to/file.rb", exception) + dispatcher.recipe_file_load_failed("/path/to/file.rb", exception) + end + + context "when an event sink has fewer arguments for an event" do + # Can't use a double because they don't report arity correctly. + let(:event_sink) do + Class.new(Chef::EventDispatch::Base) do + attr_reader :synchronized_cookbook_args + def synchronized_cookbook(cookbook_name) + @synchronized_cookbook_args = [cookbook_name] + end + end.new + end + + it "trims the arugment list" do + cookbook_version = double("cookbook_version") + dispatcher.synchronized_cookbook("apache2", cookbook_version) + expect(event_sink.synchronized_cookbook_args).to eq ["apache2"] + end + end + + end + +end + diff --git a/spec/unit/event_dispatch/dsl_spec.rb b/spec/unit/event_dispatch/dsl_spec.rb new file mode 100644 index 0000000000..0f7adce7a8 --- /dev/null +++ b/spec/unit/event_dispatch/dsl_spec.rb @@ -0,0 +1,83 @@ +# +# Author:: Ranjib Dey (<ranjib@linux.com>) +# +# Copyright:: Copyright (c) 2015 Ranjib Dey +# 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/event_dispatch/dsl' + +describe Chef::EventDispatch::DSL do + let(:events) do + Chef::EventDispatch::Dispatcher.new + end + + let(:run_context) do + Chef::RunContext.new(Chef::Node.new, nil, events) + end + + before do + Chef.set_run_context(run_context) + end + + subject{ described_class.new('test') } + + it 'set handler name' do + subject.on(:run_started) {} + expect(events.subscribers.first.name).to eq('test') + end + + it 'raise error when invalid event type is supplied' do + expect do + subject.on(:foo_bar) {} + end.to raise_error(Chef::Exceptions::InvalidEventType) + end + + it 'register user hooks against valid event type' do + subject.on(:run_failed) {'testhook'} + expect(events.subscribers.first.run_failed).to eq('testhook') + end + + it 'preserve state across event hooks' do + calls = [] + Chef.event_handler do + on :resource_updated do + calls << :updated + end + on :resource_action_start do + calls << :started + end + end + resource = Chef::Resource::RubyBlock.new('foo', run_context) + resource.block { } + resource.run_action(:run) + expect(calls).to eq([:started, :updated]) + end + + it 'preserve instance variables across handler callbacks' do + Chef.event_handler do + on :resource_action_start do + @ivar = [1] + end + on :resource_updated do + @ivar << 2 + end + end + resource = Chef::Resource::RubyBlock.new('foo', run_context) + resource.block { } + resource.run_action(:run) + expect(events.subscribers.first.instance_variable_get(:@ivar)).to eq([1, 2]) + end +end diff --git a/spec/unit/exceptions_spec.rb b/spec/unit/exceptions_spec.rb index d35ecc8ec8..85c54aa693 100644 --- a/spec/unit/exceptions_spec.rb +++ b/spec/unit/exceptions_spec.rb @@ -76,7 +76,7 @@ describe Chef::Exceptions do end if exception.methods.include?(:to_json) - include_examples "to_json equalivent to Chef::JSONCompat.to_json" do + include_examples "to_json equivalent to Chef::JSONCompat.to_json" do let(:jsonable) { exception } end end @@ -113,7 +113,7 @@ describe Chef::Exceptions do context "initialized with 1 error and nil" do let(:e) { Chef::Exceptions::RunFailedWrappingError.new(RuntimeError.new("foo"), nil) } let(:num_errors) { 1 } - let(:backtrace) { ["1) RuntimeError - foo", ""] } + let(:backtrace) { ["1) RuntimeError - foo"] } include_examples "RunFailedWrappingError expectations" end @@ -121,7 +121,7 @@ describe Chef::Exceptions do context "initialized with 2 errors" do let(:e) { Chef::Exceptions::RunFailedWrappingError.new(RuntimeError.new("foo"), RuntimeError.new("bar")) } let(:num_errors) { 2 } - let(:backtrace) { ["1) RuntimeError - foo", "", "2) RuntimeError - bar", ""] } + let(:backtrace) { ["1) RuntimeError - foo", "", "2) RuntimeError - bar"] } include_examples "RunFailedWrappingError expectations" end diff --git a/spec/unit/file_content_management/deploy/mv_windows_spec.rb b/spec/unit/file_content_management/deploy/mv_windows_spec.rb index c52001cd26..2d1981befc 100644 --- a/spec/unit/file_content_management/deploy/mv_windows_spec.rb +++ b/spec/unit/file_content_management/deploy/mv_windows_spec.rb @@ -115,6 +115,66 @@ describe Chef::FileContentManagement::Deploy::MvWindows do end + context "and the target file has null dacl and sacl" do + + before do + allow(target_file_security_descriptor).to receive(:dacl_present?).and_return(true) + allow(target_file_security_descriptor).to receive(:dacl).and_return(nil) + allow(target_file_security_descriptor).to receive(:dacl_inherits?).and_return(false) + + allow(target_file_security_descriptor).to receive(:sacl_present?).and_return(true) + allow(target_file_security_descriptor).to receive(:sacl).and_return(nil) + allow(target_file_security_descriptor).to receive(:sacl_inherits?).and_return(false) + + expect(updated_target_security_object).to receive(:set_dacl).with(nil, false) + expect(updated_target_security_object).to receive(:set_sacl).with(nil, false) + end + + + it "fixes up permissions and moves the file into place" do + content_deployer.deploy(staging_file_path, target_file_path) + end + + end + + context "and the target has an empty dacl and sacl" do + let(:original_target_file_dacl) { [] } + let(:original_target_file_sacl) { [] } + + let(:empty_dacl) { double("Windows ACL with no dacl ACEs") } + let(:empty_sacl) { double("Windows ACL with no sacl ACEs") } + + before do + allow(target_file_security_descriptor).to receive(:dacl_present?).and_return(true) + allow(target_file_security_descriptor).to receive(:dacl_inherits?).and_return(false) + + allow(target_file_security_descriptor).to receive(:dacl).and_return(original_target_file_dacl) + expect(Chef::ReservedNames::Win32::Security::ACL). + to receive(:create). + with([]). + and_return(empty_dacl) + + + allow(target_file_security_descriptor).to receive(:sacl_present?).and_return(true) + allow(target_file_security_descriptor).to receive(:sacl_inherits?).and_return(false) + + allow(target_file_security_descriptor).to receive(:sacl).and_return(original_target_file_sacl) + expect(Chef::ReservedNames::Win32::Security::ACL). + to receive(:create). + with([]). + and_return(empty_sacl) + + + expect(updated_target_security_object).to receive(:set_dacl).with(empty_dacl, false) + expect(updated_target_security_object).to receive(:set_sacl).with(empty_sacl, false) + end + + + it "fixes up permissions and moves the file into place" do + content_deployer.deploy(staging_file_path, target_file_path) + end + end + context "and the target has a dacl and sacl" do let(:inherited_dacl_ace) { double("Windows dacl ace (inherited)", :inherited? => true) } let(:not_inherited_dacl_ace) { double("Windows dacl ace (not inherited)", :inherited? => false) } diff --git a/spec/unit/formatters/doc_spec.rb b/spec/unit/formatters/doc_spec.rb new file mode 100644 index 0000000000..eb98f5abd3 --- /dev/null +++ b/spec/unit/formatters/doc_spec.rb @@ -0,0 +1,52 @@ +# +# Author:: Daniel DeLeo (<dan@chef.io>) +# +# 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' + +describe Chef::Formatters::Base do + + let(:out) { StringIO.new } + let(:err) { StringIO.new } + + subject(:formatter) { Chef::Formatters::Doc.new(out, err) } + + it "prints a policyfile's name and revision ID" do + minimal_policyfile = { + "revision_id"=> "613f803bdd035d574df7fa6da525b38df45a74ca82b38b79655efed8a189e073", + "name"=> "jenkins", + "run_list"=> [ + "recipe[apt::default]", + "recipe[java::default]", + "recipe[jenkins::master]", + "recipe[policyfile_demo::default]" + ], + "cookbook_locks"=> { } + } + + formatter.policyfile_loaded(minimal_policyfile) + expect(out.string).to include("Using policy 'jenkins' at revision '613f803bdd035d574df7fa6da525b38df45a74ca82b38b79655efed8a189e073'") + end + + it "prints cookbook name and version" do + cookbook_version = double(name: "apache2", version: "1.2.3") + formatter.synchronized_cookbook("apache2", cookbook_version) + expect(out.string).to include("- apache2 (1.2.3") + end + +end diff --git a/spec/unit/formatters/error_inspectors/api_error_formatting_spec.rb b/spec/unit/formatters/error_inspectors/api_error_formatting_spec.rb new file mode 100644 index 0000000000..b8c2de2b8b --- /dev/null +++ b/spec/unit/formatters/error_inspectors/api_error_formatting_spec.rb @@ -0,0 +1,77 @@ +# +# Author:: Tyler Cloke (<tyler@chef.io>) +# 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/formatters/error_inspectors/api_error_formatting' + +describe Chef::Formatters::APIErrorFormatting do + let(:class_instance) { (Class.new { include Chef::Formatters::APIErrorFormatting }).new } + let(:error_description) { instance_double(Chef::Formatters::ErrorDescription) } + let(:response) { double("response") } + before do + allow(response).to receive(:body) + end + + + context "when describe_406_error is called" do + context "when response['x-ops-server-api-version'] exists" do + let(:min_version) { "2" } + let(:max_version) { "5" } + let(:request_version) { "30" } + let(:return_hash) { + { + "min_version" => min_version, + "max_version" => max_version, + "request_version" => request_version + } + } + + before do + # mock out the header + allow(response).to receive(:[]).with('x-ops-server-api-version').and_return(Chef::JSONCompat.to_json(return_hash)) + end + + it "prints an error about client and server API version incompatibility with a min API version" do + expect(error_description).to receive(:section).with("Incompatible server API version:",/a min API version of #{min_version}/) + class_instance.describe_406_error(error_description, response) + end + + it "prints an error about client and server API version incompatibility with a max API version" do + expect(error_description).to receive(:section).with("Incompatible server API version:",/a max API version of #{max_version}/) + class_instance.describe_406_error(error_description, response) + end + + it "prints an error describing the request API version" do + expect(error_description).to receive(:section).with("Incompatible server API version:",/a request with an API version of #{request_version}/) + class_instance.describe_406_error(error_description, response) + end + end + + context "when response.body['error'] != 'invalid-x-ops-server-api-version'" do + + before do + allow(response).to receive(:[]).with('x-ops-server-api-version').and_return(nil) + end + + it "forwards the error_description to describe_http_error" do + expect(class_instance).to receive(:describe_http_error).with(error_description) + class_instance.describe_406_error(error_description, response) + end + end + end +end diff --git a/spec/unit/formatters/error_inspectors/compile_error_inspector_spec.rb b/spec/unit/formatters/error_inspectors/compile_error_inspector_spec.rb index ac19e91922..3c8d5dfa29 100644 --- a/spec/unit/formatters/error_inspectors/compile_error_inspector_spec.rb +++ b/spec/unit/formatters/error_inspectors/compile_error_inspector_spec.rb @@ -37,69 +37,148 @@ end E describe Chef::Formatters::ErrorInspectors::CompileErrorInspector do - before do - @node_name = "test-node.example.com" - @description = Chef::Formatters::ErrorDescription.new("Error Evaluating File:") - @exception = NoMethodError.new("undefined method `this_is_not_a_valid_method' for Chef::Resource::File") - @outputter = Chef::Formatters::IndentableOutputStream.new(StringIO.new, STDERR) - #@outputter = Chef::Formatters::IndentableOutputStream.new(STDOUT, STDERR) - end + let(:node_name) { "test-node.example.com" } - describe "when scrubbing backtraces" do - it "shows backtrace lines from cookbook files" do - # Error inspector originally used file_cache_path which is incorrect on - # chef-solo. Using cookbook_path should do the right thing for client and - # solo. - allow(Chef::Config).to receive(:cookbook_path).and_return([ "/home/someuser/dev-laptop/cookbooks" ]) - @trace = [ - "/home/someuser/dev-laptop/cookbooks/syntax-err/recipes/default.rb:14:in `from_file'", - "/home/someuser/dev-laptop/cookbooks/syntax-err/recipes/default.rb:11:in `from_file'", - "/home/someuser/.multiruby/gems/chef/lib/chef/client.rb:123:in `run'" - ] - @exception.set_backtrace(@trace) - @path = "/var/chef/cache/cookbooks/syntax-err/recipes/default.rb" - @inspector = described_class.new(@path, @exception) + let(:description) { Chef::Formatters::ErrorDescription.new("Error Evaluating File:") } - @expected_filtered_trace = [ - "/home/someuser/dev-laptop/cookbooks/syntax-err/recipes/default.rb:14:in `from_file'", - "/home/someuser/dev-laptop/cookbooks/syntax-err/recipes/default.rb:11:in `from_file'", - ] - expect(@inspector.filtered_bt).to eq(@expected_filtered_trace) - end + let(:exception) do + e = NoMethodError.new("undefined method `this_is_not_a_valid_method' for Chef::Resource::File") + e.set_backtrace(trace) + e end - describe "when explaining an error in the compile phase" do - before do - allow(Chef::Config).to receive(:cookbook_path).and_return([ "/var/chef/cache/cookbooks" ]) - recipe_lines = BAD_RECIPE.split("\n").map {|l| l << "\n" } - expect(IO).to receive(:readlines).with("/var/chef/cache/cookbooks/syntax-err/recipes/default.rb").and_return(recipe_lines) - @trace = [ - "/var/chef/cache/cookbooks/syntax-err/recipes/default.rb:14:in `from_file'", - "/var/chef/cache/cookbooks/syntax-err/recipes/default.rb:11:in `from_file'", - "/usr/local/lib/ruby/gems/chef/lib/chef/client.rb:123:in `run'" # should not display - ] - @exception.set_backtrace(@trace) - @path = "/var/chef/cache/cookbooks/syntax-err/recipes/default.rb" - @inspector = described_class.new(@path, @exception) - @inspector.add_explanation(@description) + # Change to $stdout to print error messages for manual inspection + let(:stdout) { StringIO.new } + + let(:outputter) { Chef::Formatters::IndentableOutputStream.new(StringIO.new, STDERR) } + + subject(:inspector) { described_class.new(path_to_failed_file, exception) } + + describe "finding the code responsible for the error" do + + context "when the stacktrace includes cookbook files" do + + let(:trace) do + [ + "/home/someuser/dev-laptop/cookbooks/syntax-err/recipes/default.rb:14:in `from_file'", + "/home/someuser/dev-laptop/cookbooks/syntax-err/recipes/default.rb:11:in `from_file'", + "/home/someuser/.multiruby/gems/chef/lib/chef/client.rb:123:in `run'" + ] + end + + let(:expected_filtered_trace) do + [ + "/home/someuser/dev-laptop/cookbooks/syntax-err/recipes/default.rb:14:in `from_file'", + "/home/someuser/dev-laptop/cookbooks/syntax-err/recipes/default.rb:11:in `from_file'", + ] + end + + let(:path_to_failed_file) { "/home/someuser/dev-laptop/cookbooks/syntax-err/recipes/default.rb" } + + before do + # Error inspector originally used file_cache_path which is incorrect on + # chef-solo. Using cookbook_path should do the right thing for client and + # solo. + allow(Chef::Config).to receive(:cookbook_path).and_return([ "/home/someuser/dev-laptop/cookbooks" ]) + end + + describe "when scrubbing backtraces" do + it "shows backtrace lines from cookbook files" do + expect(inspector.filtered_bt).to eq(expected_filtered_trace) + end + end + + describe "when explaining an error in the compile phase" do + before do + recipe_lines = BAD_RECIPE.split("\n").map {|l| l << "\n" } + expect(IO).to receive(:readlines).with(path_to_failed_file).and_return(recipe_lines) + inspector.add_explanation(description) + end + + it "reports the error was not located within cookbooks" do + expect(inspector.found_error_in_cookbooks?).to be(true) + end + + it "finds the line number of the error from the stacktrace" do + expect(inspector.culprit_line).to eq(14) + end + + it "prints a pretty message" do + description.display(outputter) + end + end end - it "finds the line number of the error from the stacktrace" do - expect(@inspector.culprit_line).to eq(14) + context "when the error is a RuntimeError about frozen object" do + let(:exception) do + e = RuntimeError.new("can't modify frozen Array") + e.set_backtrace(trace) + e + end + + let(:path_to_failed_file) { "/tmp/kitchen/cache/cookbooks/foo/recipes/default.rb" } + + let(:trace) do + [ + "/tmp/kitchen/cache/cookbooks/foo/recipes/default.rb:2:in `block in from_file'", + "/tmp/kitchen/cache/cookbooks/foo/recipes/default.rb:1:in `from_file'" + ] + end + + describe "when explaining a runtime error in the compile phase" do + it "correctly detects RuntimeError for frozen objects" do + expect(inspector.exception_message_modifying_frozen?).to be(true) + end + + # could also test for description.section to be called, but would have + # to adjust every other test to begin using a test double for description + end end - it "prints a pretty message" do - @description.display(@outputter) + context "when the error does not contain any lines from cookbooks" do + + let(:trace) do + [ + "/opt/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:144:in `rescue in block in load_libraries'", + "/opt/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:138:in `block in load_libraries'", + "/opt/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:230:in `call'", + "/opt/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:230:in `block (2 levels) in foreach_cookbook_load_segment'", + "/opt/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:229:in `each'", + "/opt/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:229:in `block in foreach_cookbook_load_segment'", + "/opt/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:227:in `each'", + "/opt/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:227:in `foreach_cookbook_load_segment'", + "/opt/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:137:in `load_libraries'" + ] + end + + let(:exception) do + e = Chef::Exceptions::RecipeNotFound.new("recipe nope:nope not found") + e.set_backtrace(trace) + e + end + + let(:path_to_failed_file) { nil } + + it "gives a full, non-filtered trace" do + expect(inspector.filtered_bt).to eq(trace) + end + + it "does not error when displaying the error" do + expect { description.display(outputter) }.to_not raise_error + end + + it "reports the error was not located within cookbooks" do + expect(inspector.found_error_in_cookbooks?).to be(false) + end + end end describe "when explaining an error on windows" do - before do - allow(Chef::Config).to receive(:cookbook_path).and_return([ "C:/opscode/chef/var/cache/cookbooks" ]) - recipe_lines = BAD_RECIPE.split("\n").map {|l| l << "\n" } - expect(IO).to receive(:readlines).at_least(1).times.with(/:\/opscode\/chef\/var\/cache\/cookbooks\/foo\/recipes\/default.rb/).and_return(recipe_lines) - @trace = [ + + let(:trace_with_upcase_drive) do + [ "C:/opscode/chef/var/cache/cookbooks/foo/recipes/default.rb:14 in `from_file'", "C:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:144:in `rescue in block in load_libraries'", "C:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:138:in `block in load_libraries'", @@ -122,81 +201,65 @@ describe Chef::Formatters::ErrorInspectors::CompileErrorInspector do "C:/opscode/chef/bin/chef-client:19:in `load'", "C:/opscode/chef/bin/chef-client:19:in `<main>'" ] - @exception.set_backtrace(@trace) - @path = "/var/chef/cache/cookbooks/syntax-err/recipes/default.rb" - @inspector = described_class.new(@path, @exception) - @inspector.add_explanation(@description) end + let(:trace) { trace_with_upcase_drive } + + let(:path_to_failed_file) { "/var/cache/cookbooks/foo/recipes/default.rb" } - describe "and examining the stack trace for a recipe" do - it "find the culprit recipe name when the drive letter is upper case" do - expect(@inspector.culprit_file).to eq("C:/opscode/chef/var/cache/cookbooks/foo/recipes/default.rb") + before do + allow(Chef::Config).to receive(:cookbook_path).and_return([ "C:/opscode/chef/var/cache/cookbooks" ]) + recipe_lines = BAD_RECIPE.split("\n").map {|l| l << "\n" } + expect(IO).to receive(:readlines).at_least(1).times.with(full_path_to_failed_file).and_return(recipe_lines) + inspector.add_explanation(description) + end + + context "when the drive letter in the path is uppercase" do + + let(:full_path_to_failed_file) { "C:/opscode/chef#{path_to_failed_file}" } + + it "reports the error was not located within cookbooks" do + expect(inspector.found_error_in_cookbooks?).to be(true) end - it "find the culprit recipe name when the drive letter is lower case" do - @trace.each { |line| line.gsub!(/^C:/, "c:") } - @exception.set_backtrace(@trace) - @inspector = described_class.new(@path, @exception) - @inspector.add_explanation(@description) - expect(@inspector.culprit_file).to eq("c:/opscode/chef/var/cache/cookbooks/foo/recipes/default.rb") + it "finds the culprit recipe name" do + expect(inspector.culprit_file).to eq("C:/opscode/chef/var/cache/cookbooks/foo/recipes/default.rb") end - end - it "finds the line number of the error from the stack trace" do - expect(@inspector.culprit_line).to eq(14) - end + it "finds the line number of the error from the stack trace" do + expect(inspector.culprit_line).to eq(14) + end - it "prints a pretty message" do - @description.display(@outputter) + it "prints a pretty message" do + description.display(outputter) + end end - end - describe "when explaining an error on windows, and the backtrace lowercases the drive letter" do - before do - allow(Chef::Config).to receive(:cookbook_path).and_return([ "C:/opscode/chef/var/cache/cookbooks" ]) - recipe_lines = BAD_RECIPE.split("\n").map {|l| l << "\n" } - expect(IO).to receive(:readlines).with("c:/opscode/chef/var/cache/cookbooks/foo/recipes/default.rb").and_return(recipe_lines) - @trace = [ - "c:/opscode/chef/var/cache/cookbooks/foo/recipes/default.rb:14 in `from_file'", - "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:144:in `rescue in block in load_libraries'", - "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:138:in `block in load_libraries'", - "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:230:in `call'", - "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:230:in `block (2 levels) in foreach_cookbook_load_segment'", - "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:229:in `each'", - "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:229:in `block in foreach_cookbook_load_segment'", - "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:227:in `each'", - "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:227:in `foreach_cookbook_load_segment'", - "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:137:in `load_libraries'", - "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:62:in `load'", - "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/client.rb:198:in `setup_run_context'", - "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/client.rb:418:in `do_run'", - "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/client.rb:176:in `run'", - "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/application/client.rb:283:in `block in run_application'", - "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/application/client.rb:270:in `loop'", - "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/application/client.rb:270:in `run_application'", - "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/application.rb:70:in `run'", - "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/bin/chef-client:26:in `<top (required)>'", - "c:/opscode/chef/bin/chef-client:19:in `load'", - "c:/opscode/chef/bin/chef-client:19:in `<main>'" - ] - @exception.set_backtrace(@trace) - @path = "/var/chef/cache/cookbooks/syntax-err/recipes/default.rb" - @inspector = described_class.new(@path, @exception) - @inspector.add_explanation(@description) - end + context "when the drive letter in the path is lowercase" do - it "finds the culprit recipe name from the stacktrace" do - expect(@inspector.culprit_file).to eq("c:/opscode/chef/var/cache/cookbooks/foo/recipes/default.rb") - end + let(:trace) do + trace_with_upcase_drive.map { |line| line.gsub(/^C:/, "c:") } + end - it "finds the line number of the error from the stack trace" do - expect(@inspector.culprit_line).to eq(14) - end + let(:full_path_to_failed_file) { "c:/opscode/chef#{path_to_failed_file}" } + + it "reports the error was not located within cookbooks" do + expect(inspector.found_error_in_cookbooks?).to be(true) + end + + it "finds the culprit recipe name from the stacktrace" do + expect(inspector.culprit_file).to eq("c:/opscode/chef/var/cache/cookbooks/foo/recipes/default.rb") + end - it "prints a pretty message" do - @description.display(@outputter) + it "finds the line number of the error from the stack trace" do + expect(inspector.culprit_line).to eq(14) + end + + it "prints a pretty message" do + description.display(outputter) + end end + end end diff --git a/spec/unit/formatters/error_inspectors/resource_failure_inspector_spec.rb b/spec/unit/formatters/error_inspectors/resource_failure_inspector_spec.rb index a42d234601..5594d6e18a 100644 --- a/spec/unit/formatters/error_inspectors/resource_failure_inspector_spec.rb +++ b/spec/unit/formatters/error_inspectors/resource_failure_inspector_spec.rb @@ -126,6 +126,13 @@ describe Chef::Formatters::ErrorInspectors::ResourceFailureInspector do expect(@inspector.recipe_snippet).to match(/^# In C:\/Users\/btm/) end + it "parses a Windows path" do + source_line = "C:\\Windows\\Temp\\packer\\cookbooks\\fake_file.rb:2: undefined local variable or method `non_existent' for main:Object (NameError)" + @resource.source_line = source_line + @inspector = Chef::Formatters::ErrorInspectors::ResourceFailureInspector.new(@resource, :create, @exception) + expect(@inspector.recipe_snippet).to match(/^# In C:\\Windows\\Temp\\packer\\/) + end + it "parses a unix path" do source_line = "/home/btm/src/chef/chef/spec/unit/fake_file.rb:2: undefined local variable or method `non_existent' for main:Object (NameError)" @resource.source_line = source_line diff --git a/spec/unit/guard_interpreter/resource_guard_interpreter_spec.rb b/spec/unit/guard_interpreter/resource_guard_interpreter_spec.rb index 4cf3ba827a..acf1b15fd8 100644 --- a/spec/unit/guard_interpreter/resource_guard_interpreter_spec.rb +++ b/spec/unit/guard_interpreter/resource_guard_interpreter_spec.rb @@ -24,6 +24,7 @@ describe Chef::GuardInterpreter::ResourceGuardInterpreter do node.default["kernel"] = Hash.new node.default["kernel"][:machine] = :x86_64.to_s + node.automatic[:os] = 'windows' node end @@ -83,6 +84,14 @@ describe Chef::GuardInterpreter::ResourceGuardInterpreter do expect(guard_interpreter.evaluate).to eq(true) end + it "does not corrupt the run_context of the node" do + node_run_context_before_guard_execution = parent_resource.run_context + expect(node_run_context_before_guard_execution.object_id).to eq(parent_resource.node.run_context.object_id) + guard_interpreter.evaluate + node_run_context_after_guard_execution = parent_resource.run_context + expect(node_run_context_after_guard_execution.object_id).to eq(parent_resource.node.run_context.object_id) + end + describe "script command opts switch" do let(:command_opts) { {} } let(:guard_interpreter) { Chef::GuardInterpreter::ResourceGuardInterpreter.new(parent_resource, "exit 0", command_opts) } @@ -144,4 +153,3 @@ describe Chef::GuardInterpreter::ResourceGuardInterpreter do end end end - diff --git a/spec/unit/http/authenticator_spec.rb b/spec/unit/http/authenticator_spec.rb new file mode 100644 index 0000000000..48bbdcf76c --- /dev/null +++ b/spec/unit/http/authenticator_spec.rb @@ -0,0 +1,78 @@ +# +# Author:: Tyler Cloke (<tyler@chef.io>) +# 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/http/authenticator' + +describe Chef::HTTP::Authenticator do + let(:class_instance) { Chef::HTTP::Authenticator.new } + let(:method) { double("method") } + let(:url) { double("url") } + let(:headers) { Hash.new } + let(:data) { double("data") } + + before do + allow(class_instance).to receive(:authentication_headers).and_return({}) + end + + context "when handle_request is called" do + shared_examples_for "merging the server API version into the headers" do + it "merges the default version of X-Ops-Server-API-Version into the headers" do + # headers returned + expect(class_instance.handle_request(method, url, headers, data)[2]). + to include({'X-Ops-Server-API-Version' => Chef::HTTP::Authenticator::DEFAULT_SERVER_API_VERSION}) + end + + context "when api_version is set to something other than the default" do + let(:class_instance) { Chef::HTTP::Authenticator.new({:api_version => '-10'}) } + + it "merges the requested version of X-Ops-Server-API-Version into the headers" do + expect(class_instance.handle_request(method, url, headers, data)[2]). + to include({'X-Ops-Server-API-Version' => '-10'}) + end + end + end + + context "when !sign_requests?" do + before do + allow(class_instance).to receive(:sign_requests?).and_return(false) + end + + it_behaves_like "merging the server API version into the headers" + + it "authentication_headers is not called" do + expect(class_instance).to_not receive(:authentication_headers) + class_instance.handle_request(method, url, headers, data) + end + + end + + context "when sign_requests?" do + before do + allow(class_instance).to receive(:sign_requests?).and_return(true) + end + + it_behaves_like "merging the server API version into the headers" + + it "calls authentication_headers with the proper input" do + expect(class_instance).to receive(:authentication_headers).with(method, url, data).and_return({}) + class_instance.handle_request(method, url, headers, data) + end + end + end +end diff --git a/spec/unit/http/basic_client_spec.rb b/spec/unit/http/basic_client_spec.rb index eb133f943e..b7552f54aa 100644 --- a/spec/unit/http/basic_client_spec.rb +++ b/spec/unit/http/basic_client_spec.rb @@ -21,7 +21,7 @@ require 'chef/http/basic_client' describe "HTTP Connection" do let(:uri) { URI("https://example.com:4443") } - subject { Chef::HTTP::BasicClient.new(uri) } + subject(:basic_client) { Chef::HTTP::BasicClient.new(uri) } describe ".new" do it "creates an instance" do @@ -45,11 +45,6 @@ describe "HTTP Connection" do let(:proxy_port) { 8080 } let(:proxy) { "#{proxy_prefix}#{proxy_host}:#{proxy_port}" } - before do - Chef::Config["#{uri.scheme}_proxy"] = proxy - Chef::Config[:no_proxy] = nil - end - it "should contain the host" do proxy_uri = subject.proxy_uri expect(proxy_uri.host).to eq(proxy_host) @@ -63,13 +58,71 @@ describe "HTTP Connection" do context "when the config setting is normalized (does not contain the scheme)" do include_examples "a proxy uri" do + let(:proxy_prefix) { "" } + + before do + Chef::Config["#{uri.scheme}_proxy"] = proxy + Chef::Config[:no_proxy] = nil + end + end end context "when the config setting is not normalized (contains the scheme)" do include_examples "a proxy uri" do let(:proxy_prefix) { "#{uri.scheme}://" } + + before do + Chef::Config["#{uri.scheme}_proxy"] = proxy + Chef::Config[:no_proxy] = nil + end + + end + end + + context "when the proxy is set by the environment" do + + include_examples "a proxy uri" do + + let(:env) do + { + "https_proxy" => "https://proxy.mycorp.com:8080", + "https_proxy_user" => "jane_username", + "https_proxy_pass" => "opensesame" + } + end + + let(:proxy_uri) { URI.parse(env["https_proxy"]) } + + before do + allow(basic_client).to receive(:env).and_return(env) + end + + it "sets the proxy user" do + expect(basic_client.http_proxy_user(proxy_uri)).to eq("jane_username") + end + + it "sets the proxy pass" do + expect(basic_client.http_proxy_pass(proxy_uri)).to eq("opensesame") + end + end + + end + + context "when an empty proxy is set by the environment" do + let(:env) do + { + "https_proxy" => "" + } + end + + before do + allow(subject).to receive(:env).and_return(env) + end + + it "to not fail with URI parse exception" do + expect { subject.proxy_uri }.to_not raise_error end end end diff --git a/spec/unit/http/socketless_chef_zero_client_spec.rb b/spec/unit/http/socketless_chef_zero_client_spec.rb new file mode 100644 index 0000000000..963cc9e8c4 --- /dev/null +++ b/spec/unit/http/socketless_chef_zero_client_spec.rb @@ -0,0 +1,174 @@ +#-- +# Author:: Daniel DeLeo (<dan@chef.io>) +# 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 'chef/http/socketless_chef_zero_client' + +describe Chef::HTTP::SocketlessChefZeroClient do + + let(:relative_url) { "" } + let(:uri_str) { "chefzero://localhost:1/#{relative_url}" } + let(:uri) { URI(uri_str) } + + subject(:zero_client) { Chef::HTTP::SocketlessChefZeroClient.new(uri) } + + it "has a host" do + expect(zero_client.host).to eq("localhost") + end + + it "has a port" do + expect(zero_client.port).to eq(1) + end + + describe "converting requests to rack format" do + + let(:expected_rack_req) do + { + "SCRIPT_NAME" => "", + "SERVER_NAME" => "localhost", + "REQUEST_METHOD" => method.to_s.upcase, + "PATH_INFO" => uri.path, + "QUERY_STRING" => uri.query, + "SERVER_PORT" => uri.port, + "HTTP_HOST" => "localhost:#{uri.port}", + "rack.url_scheme" => "chefzero", + } + end + + context "when the request has no body" do + + let(:method) { :GET } + let(:relative_url) { "clients" } + let(:headers) { { "Accept" => "application/json" } } + let(:body) { false } + let(:expected_body_str) { "" } + + let(:rack_req) { zero_client.req_to_rack(method, uri, body, headers) } + + it "creates a rack request env" do + # StringIO doesn't implement == in a way that we can compare, so we + # check rack.input individually and then iterate over everything else + expect(rack_req["rack.input"].string).to eq(expected_body_str) + expected_rack_req.each do |key, value| + expect(rack_req[key]).to eq(value) + end + end + + end + + context "when the request has a body" do + + let(:method) { :PUT } + let(:relative_url) { "clients/foo" } + let(:headers) { { "Accept" => "application/json" } } + let(:body) { "bunch o' JSON" } + let(:expected_body_str) { "bunch o' JSON" } + + let(:rack_req) { zero_client.req_to_rack(method, uri, body, headers) } + + it "creates a rack request env" do + # StringIO doesn't implement == in a way that we can compare, so we + # check rack.input individually and then iterate over everything else + expect(rack_req["rack.input"].string).to eq(expected_body_str) + expected_rack_req.each do |key, value| + expect(rack_req[key]).to eq(value) + end + end + + end + + end + + describe "converting responses to Net::HTTP objects" do + + let(:net_http_response) { zero_client.to_net_http(code, headers, body) } + + context "when the request was successful (2XX)" do + + let(:code) { 200 } + let(:headers) { { "Content-Type" => "Application/JSON" } } + let(:body) { [ "bunch o' JSON" ] } + + it "creates a Net::HTTP success response object" do + expect(net_http_response).to be_a_kind_of(Net::HTTPOK) + expect(net_http_response.read_body).to eq("bunch o' JSON") + expect(net_http_response["content-type"]).to eq("Application/JSON") + end + + it "does not fail when calling read_body with a block" do + expect(net_http_response.read_body {|chunk| chunk }).to eq("bunch o' JSON") + end + + end + + context "when the requested object doesn't exist (404)" do + + let(:code) { 404 } + let(:headers) { { "Content-Type" => "Application/JSON" } } + let(:body) { [ "nope" ] } + + it "creates a Net::HTTPNotFound response object" do + expect(net_http_response).to be_a_kind_of(Net::HTTPNotFound) + end + end + + end + + describe "request-response round trip" do + + let(:method) { :GET } + let(:relative_url) { "clients" } + let(:headers) { { "Accept" => "application/json" } } + let(:body) { false } + + let(:expected_rack_req) do + { + "SCRIPT_NAME" => "", + "SERVER_NAME" => "localhost", + "REQUEST_METHOD" => method.to_s.upcase, + "PATH_INFO" => uri.path, + "QUERY_STRING" => uri.query, + "SERVER_PORT" => uri.port, + "HTTP_HOST" => "localhost:#{uri.port}", + "rack.url_scheme" => "chefzero", + "rack.input" => an_instance_of(StringIO), + } + end + + + let(:response_code) { 200 } + let(:response_headers) { { "Content-Type" => "Application/JSON" } } + let(:response_body) { [ "bunch o' JSON" ] } + + let(:rack_response) { [ response_code, response_headers, response_body ] } + + let(:response) { zero_client.request(method, uri, body, headers) } + + before do + expect(ChefZero::SocketlessServerMap).to receive(:request).with(1, expected_rack_req).and_return(rack_response) + end + + it "makes a rack request to Chef Zero and returns the response as a Net::HTTP object" do + _client, net_http_response = response + expect(net_http_response).to be_a_kind_of(Net::HTTPOK) + expect(net_http_response.code).to eq("200") + expect(net_http_response.body).to eq("bunch o' JSON") + end + + end + +end diff --git a/spec/unit/http_spec.rb b/spec/unit/http_spec.rb index ddfc56583d..4d851df951 100644 --- a/spec/unit/http_spec.rb +++ b/spec/unit/http_spec.rb @@ -20,6 +20,7 @@ require 'spec_helper' require 'chef/http' require 'chef/http/basic_client' +require 'chef/http/socketless_chef_zero_client' class Chef::HTTP public :create_url @@ -27,6 +28,19 @@ end describe Chef::HTTP do + context "when given a chefzero:// URL" do + + let(:uri) { URI("chefzero://localhost:1") } + + subject(:http) { Chef::HTTP.new(uri) } + + it "uses the SocketlessChefZeroClient to handle requests" do + expect(http.http_client).to be_a_kind_of(Chef::HTTP::SocketlessChefZeroClient) + expect(http.http_client.url).to eq(uri) + end + + end + describe "create_url" do it 'should return a correctly formatted url 1/3 CHEF-5261' do diff --git a/spec/unit/json_compat_spec.rb b/spec/unit/json_compat_spec.rb index 65d931df70..fd6469c146 100644 --- a/spec/unit/json_compat_spec.rb +++ b/spec/unit/json_compat_spec.rb @@ -67,37 +67,25 @@ describe Chef::JSONCompat do expect(Chef::JSONCompat.to_json_pretty(f)).to eql("{\n \"foo\": 1234,\n \"bar\": {\n \"baz\": 5678\n }\n}\n") end - include_examples "to_json equalivent to Chef::JSONCompat.to_json" do + include_examples "to_json equivalent to Chef::JSONCompat.to_json" do let(:jsonable) { Foo.new } end end - describe "with a file with 300 or less nested entries" do - let(:json) { IO.read(File.join(CHEF_SPEC_DATA, 'big_json.json')) } + describe "with the file with 252 or less nested entries" do + let(:json) { IO.read(File.join(CHEF_SPEC_DATA, 'nested.json')) } let(:hash) { Chef::JSONCompat.from_json(json) } - describe "when a big json file is loaded" do + describe "when the 252 json file is loaded" do it "should create a Hash from the file" do expect(hash).to be_kind_of(Hash) end - it "should has 'test' as a 300th nested value" do - expect(hash['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']).to eq('test') - end - end - end - - describe "with a file with more than 300 nested entries" do - let(:json) { IO.read(File.join(CHEF_SPEC_DATA, 'big_json_plus_one.json')) } - let(:hash) { Chef::JSONCompat.from_json(json, {:max_nesting => 301}) } - - describe "when a big json file is loaded" do - it "should create a Hash from the file" do - expect(hash).to be_kind_of(Hash) - end - - it "should has 'test' as a 301st nested value" do - expect(hash['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']).to eq('test') + it "should has 'test' as a 252 nested value" do + v = 252.times.inject(hash) do |memo, _| + memo['key'] + end + expect(v).to eq('test') end end end diff --git a/spec/unit/key_spec.rb b/spec/unit/key_spec.rb new file mode 100644 index 0000000000..94ebbf6ae8 --- /dev/null +++ b/spec/unit/key_spec.rb @@ -0,0 +1,634 @@ +# +# Author:: Tyler Cloke (tyler@chef.io) +# 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/key' + +describe Chef::Key do + # whether user or client irrelevent to these tests + let(:key) { Chef::Key.new("original_actor", "user") } + let(:public_key_string) do + <<EOS +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvPo+oNPB7uuNkws0fC02 +KxSwdyqPLu0fhI1pOweNKAZeEIiEz2PkybathHWy8snSXGNxsITkf3eyvIIKa8OZ +WrlqpI3yv/5DOP8HTMCxnFuMJQtDwMcevlqebX4bCxcByuBpNYDcAHjjfLGSfMjn +E5lZpgYWwnpic4kSjYcL9ORK9nYvlWV9P/kCYmRhIjB4AhtpWRiOfY/TKi3P2LxT +IjSmiN/ihHtlhV/VSnBJ5PzT/lRknlrJ4kACoz7Pq9jv+aAx5ft/xE9yDa2DYs0q +Tfuc9dUYsFjptWYrV6pfEQ+bgo1OGBXORBFcFL+2D7u9JYquKrMgosznHoEkQNLo +0wIDAQAB +-----END PUBLIC KEY----- +EOS + end + + shared_examples_for "fields with username type validation" do + context "when invalid input is passed" do + # It is not feasible to check all invalid characters. Here are a few + # that we probably care about. + it "should raise an ArgumentError" do + # capital letters + expect { key.send(field, "Bar") }.to raise_error(ArgumentError) + # slashes + expect { key.send(field, "foo/bar") }.to raise_error(ArgumentError) + # ? + expect { key.send(field, "foo?") }.to raise_error(ArgumentError) + # & + expect { key.send(field, "foo&") }.to raise_error(ArgumentError) + # spaces + expect { key.send(field, "foo ") }.to raise_error(ArgumentError) + end + end + end + + shared_examples_for "string fields that are settable" do + context "when it is set with valid input" do + it "should set the field" do + key.send(field, valid_input) + expect(key.send(field)).to eq(valid_input) + end + end + + context "when you feed it anything but a string" do + it "should raise an ArgumentError" do + expect { key.send(field, Hash.new) }.to raise_error(ArgumentError) + end + end + end + + + describe "when a new Chef::Key object is initialized with invalid input" do + it "should raise an InvalidKeyArgument" do + expect { Chef::Key.new("original_actor", "not_a_user_or_client") }.to raise_error(Chef::Exceptions::InvalidKeyArgument) + end + end + + describe "when a new Chef::Key object is initialized with valid input" do + it "should be a Chef::Key" do + expect(key).to be_a_kind_of(Chef::Key) + end + + it "should properly set the actor" do + expect(key.actor).to eq("original_actor") + end + end + + describe "when actor field is set" do + it_should_behave_like "string fields that are settable" do + let(:field) { :actor } + let(:valid_input) { "new_field_value" } + end + + it_should_behave_like "fields with username type validation" do + let(:field) { :actor } + end + end + + describe "when the name field is set" do + it_should_behave_like "string fields that are settable" do + let(:field) { :name } + let(:valid_input) { "new_field_value" } + end + end + + describe "when the private_key field is set" do + it_should_behave_like "string fields that are settable" do + let(:field) { :private_key } + let(:valid_input) { "new_field_value" } + end + end + + describe "when the public_key field is set" do + it_should_behave_like "string fields that are settable" do + let(:field) { :public_key } + let(:valid_input) { "new_field_value" } + end + + context "when create_key is true" do + before do + key.create_key true + end + + it "should raise an InvalidKeyAttribute" do + expect { key.public_key public_key_string }.to raise_error(Chef::Exceptions::InvalidKeyAttribute) + end + end + end + + describe "when the create_key field is set" do + context "when it is set to true" do + it "should set the field" do + key.create_key(true) + expect(key.create_key).to eq(true) + end + end + + context "when it is set to false" do + it "should set the field" do + key.create_key(false) + expect(key.create_key).to eq(false) + end + end + + context "when anything but a TrueClass or FalseClass is passed" do + it "should raise an ArgumentError" do + expect { key.create_key "not_a_boolean" }.to raise_error(ArgumentError) + end + end + + context "when public_key is defined" do + before do + key.public_key public_key_string + end + + it "should raise an InvalidKeyAttribute" do + expect { key.create_key true }.to raise_error(Chef::Exceptions::InvalidKeyAttribute) + end + end + end + + describe "when the expiration_date field is set" do + context "when a valid date is passed" do + it_should_behave_like "string fields that are settable" do + let(:field) { :public_key } + let(:valid_input) { "2020-12-24T21:00:00Z" } + end + end + + context "when infinity is passed" do + it_should_behave_like "string fields that are settable" do + let(:field) { :public_key } + let(:valid_input) { "infinity" } + end + end + + context "when an invalid date is passed" do + it "should raise an ArgumentError" do + expect { key.expiration_date "invalid_date" }.to raise_error(ArgumentError) + # wrong years + expect { key.expiration_date "20-12-24T21:00:00Z" }.to raise_error(ArgumentError) + end + + context "when it is a valid UTC date missing a Z" do + it "should raise an ArgumentError" do + expect { key.expiration_date "2020-12-24T21:00:00" }.to raise_error(ArgumentError) + end + end + end + end # when the expiration_date field is set + + describe "when serializing to JSON" do + shared_examples_for "common json operations" do + it "should serializes as a JSON object" do + expect(json).to match(/^\{.+\}$/) + end + + it "should include the actor value under the key relative to the actor_field_name passed" do + expect(json).to include(%Q("#{new_key.actor_field_name}":"original_actor")) + end + + it "should include the name field when present" do + new_key.name("monkeypants") + expect(new_key.to_json).to include(%q{"name":"monkeypants"}) + end + + it "should not include the name if not present" do + expect(json).to_not include("name") + end + + it "should include the public_key field when present" do + new_key.public_key "this_public_key" + expect(new_key.to_json).to include(%q("public_key":"this_public_key")) + end + + it "should not include the public_key if not present" do + expect(json).to_not include("public_key") + end + + it "should include the private_key field when present" do + new_key.private_key "this_public_key" + expect(new_key.to_json).to include(%q("private_key":"this_public_key")) + end + + it "should not include the private_key if not present" do + expect(json).to_not include("private_key") + end + + it "should include the expiration_date field when present" do + new_key.expiration_date "2020-12-24T21:00:00Z" + expect(new_key.to_json).to include(%Q("expiration_date":"2020-12-24T21:00:00Z")) + end + + it "should not include the expiration_date if not present" do + expect(json).to_not include("expiration_date") + end + + it "should include the create_key field when present" do + new_key.create_key true + expect(new_key.to_json).to include(%q("create_key":true)) + end + + it "should not include the create_key if not present" do + expect(json).to_not include("create_key") + end + end + + context "when key is for a user" do + it_should_behave_like "common json operations" do + let(:new_key) { Chef::Key.new("original_actor", "user") } + let(:json) do + new_key.to_json + end + end + end + + context "when key is for a client" do + it_should_behave_like "common json operations" do + let(:new_key) { Chef::Key.new("original_actor", "client") } + let(:json) do + new_key.to_json + end + end + end + + end # when serializing to JSON + + describe "when deserializing from JSON" do + shared_examples_for "a deserializable object" do + it "deserializes to a Chef::Key object" do + expect(key).to be_a_kind_of(Chef::Key) + end + + it "preserves the actor" do + expect(key.actor).to eq("turtle") + end + + it "preserves the name" do + expect(key.name).to eq("key_name") + end + + it "includes the public key if present" do + expect(key.public_key).to eq(public_key_string) + end + + it "includes the expiration_date if present" do + expect(key.expiration_date).to eq("infinity") + end + + it "includes the private_key if present" do + expect(key.private_key).to eq("some_private_key") + end + + it "includes the create_key if present" do + expect(key_with_create_key_field.create_key).to eq(true) + end + end + + context "when deserializing a key for a user" do + it_should_behave_like "a deserializable object" do + let(:key) do + o = { "user" => "turtle", + "name" => "key_name", + "public_key" => public_key_string, + "private_key" => "some_private_key", + "expiration_date" => "infinity"} + Chef::Key.from_json(o.to_json) + end + let(:key_with_create_key_field) do + o = { "user" => "turtle", + "create_key" => true } + Chef::Key.from_json(o.to_json) + end + end + end + + context "when deserializing a key for a client" do + it_should_behave_like "a deserializable object" do + let(:key) do + o = { "client" => "turtle", + "name" => "key_name", + "public_key" => public_key_string, + "private_key" => "some_private_key", + "expiration_date" => "infinity"} + Chef::Key.from_json(o.to_json) + end + let(:key_with_create_key_field) do + o = { "client" => "turtle", + "create_key" => true } + Chef::Key.from_json(o.to_json) + end + end + end + end # when deserializing from JSON + + + describe "API Interactions" do + let(:rest) do + Chef::Config[:chef_server_root] = "http://www.example.com" + Chef::Config[:chef_server_url] = "http://www.example.com/organizations/test_org" + r = double('rest') + allow(Chef::REST).to receive(:new).and_return(r) + r + end + + let(:user_key) do + o = Chef::Key.new("foobar", "user") + o + end + + let(:client_key) do + o = Chef::Key.new("foobar", "client") + o + end + + describe "list" do + context "when listing keys for a user" do + let(:response) { [{"uri" => "http://www.example.com/users/keys/foobar", "name"=>"foobar", "expired"=>false}] } + let(:inflated_response) { {"foobar" => user_key} } + + it "lists all keys" do + expect(rest).to receive(:get_rest).with("users/#{user_key.actor}/keys").and_return(response) + expect(Chef::Key.list_by_user("foobar")).to eq(response) + end + + it "inflate all keys" do + allow(Chef::Key).to receive(:load_by_user).with(user_key.actor, "foobar").and_return(user_key) + expect(rest).to receive(:get_rest).with("users/#{user_key.actor}/keys").and_return(response) + expect(Chef::Key.list_by_user("foobar", true)).to eq(inflated_response) + end + + end + + context "when listing keys for a client" do + let(:response) { [{"uri" => "http://www.example.com/users/keys/foobar", "name"=>"foobar", "expired"=>false}] } + let(:inflated_response) { {"foobar" => client_key} } + + it "lists all keys" do + expect(rest).to receive(:get_rest).with("clients/#{client_key.actor}/keys").and_return(response) + expect(Chef::Key.list_by_client("foobar")).to eq(response) + end + + it "inflate all keys" do + allow(Chef::Key).to receive(:load_by_client).with(client_key.actor, "foobar").and_return(client_key) + expect(rest).to receive(:get_rest).with("clients/#{user_key.actor}/keys").and_return(response) + expect(Chef::Key.list_by_client("foobar", true)).to eq(inflated_response) + end + + end + end + + + describe "create" do + shared_examples_for "create key" do + context "when a field is missing" do + it "should raise a MissingKeyAttribute" do + expect { key.create }.to raise_error(Chef::Exceptions::MissingKeyAttribute) + end + end + + context "when the name field is missing" do + before do + key.public_key public_key_string + key.expiration_date "2020-12-24T21:00:00Z" + end + + it "creates a new key via the API with the fingerprint as the name" do + expect(rest).to receive(:post_rest).with(url, + {"name" => "12:3e:33:73:0b:f4:ec:72:dc:f0:4c:51:62:27:08:76:96:24:f4:4a", + "public_key" => key.public_key, + "expiration_date" => key.expiration_date}).and_return({}) + key.create + end + end + + context "when every field is populated" do + before do + key.name "key_name" + key.public_key public_key_string + key.expiration_date "2020-12-24T21:00:00Z" + key.create_key false + end + + context "when create_key is false" do + it "creates a new key via the API" do + expect(rest).to receive(:post_rest).with(url, + {"name" => key.name, + "public_key" => key.public_key, + "expiration_date" => key.expiration_date}).and_return({}) + key.create + end + end + + context "when create_key is true and public_key is nil" do + + before do + key.delete_public_key + key.create_key true + $expected_output = { + actor_type => "foobar", + "name" => key.name, + "create_key" => true, + "expiration_date" => key.expiration_date + } + $expected_input = { + "name" => key.name, + "create_key" => true, + "expiration_date" => key.expiration_date + } + end + + it "should create a new key via the API" do + expect(rest).to receive(:post_rest).with(url, $expected_input).and_return({}) + key.create + end + + context "when the server returns the private_key via key.create" do + before do + allow(rest).to receive(:post_rest).with(url, $expected_input).and_return({"private_key" => "this_private_key"}) + end + + it "key.create returns the original key plus the private_key" do + expect(key.create.to_hash).to eq($expected_output.merge({"private_key" => "this_private_key"})) + end + end + end + + context "when create_key is false and public_key is nil" do + before do + key.delete_public_key + key.create_key false + end + it "should raise an InvalidKeyArgument" do + expect { key.create }.to raise_error(Chef::Exceptions::MissingKeyAttribute) + end + end + end + end + + context "when creating a user key" do + it_should_behave_like "create key" do + let(:url) { "users/#{key.actor}/keys" } + let(:key) { user_key } + let(:actor_type) { "user" } + end + end + + context "when creating a client key" do + it_should_behave_like "create key" do + let(:url) { "clients/#{client_key.actor}/keys" } + let(:key) { client_key } + let(:actor_type) { "client" } + end + end + end # create + + describe "update" do + shared_examples_for "update key" do + context "when name is missing and no argument was passed to update" do + it "should raise an MissingKeyAttribute" do + expect { key.update }.to raise_error(Chef::Exceptions::MissingKeyAttribute) + end + end + + context "when some fields are populated" do + before do + key.name "key_name" + key.expiration_date "2020-12-24T21:00:00Z" + end + + it "should update the key via the API" do + expect(rest).to receive(:put_rest).with(url, key.to_hash).and_return({}) + key.update + end + end + + context "when @name is not nil and a arg is passed to update" do + before do + key.name "new_name" + end + + it "passes @name in the body and the arg in the PUT URL" do + expect(rest).to receive(:put_rest).with(update_name_url, key.to_hash).and_return({}) + key.update("old_name") + end + end + + context "when the server returns a public_key and create_key is true" do + before do + key.name "key_name" + key.create_key true + allow(rest).to receive(:put_rest).with(url, key.to_hash).and_return({ + "key" => "key_name", + "public_key" => public_key_string + }) + + end + + it "returns a key with public_key populated" do + new_key = key.update + expect(new_key.public_key).to eq(public_key_string) + end + + it "returns a key without create_key set" do + new_key = key.update + expect(new_key.create_key).to be_nil + end + end + end + + context "when updating a user key" do + it_should_behave_like "update key" do + let(:url) { "users/#{key.actor}/keys/#{key.name}" } + let(:update_name_url) { "users/#{key.actor}/keys/old_name" } + let(:key) { user_key } + end + end + + context "when updating a client key" do + it_should_behave_like "update key" do + let(:url) { "clients/#{client_key.actor}/keys/#{key.name}" } + let(:update_name_url) { "clients/#{client_key.actor}/keys/old_name" } + let(:key) { client_key } + end + end + + end #update + + describe "load" do + shared_examples_for "load" do + it "should load a named key from the API" do + expect(rest).to receive(:get_rest).with(url).and_return({"user" => "foobar", "name" => "test_key_name", "public_key" => public_key_string, "expiration_date" => "infinity"}) + key = Chef::Key.send(load_method, "foobar", "test_key_name") + expect(key.actor).to eq("foobar") + expect(key.name).to eq("test_key_name") + expect(key.public_key).to eq(public_key_string) + expect(key.expiration_date).to eq("infinity") + end + end + + describe "load_by_user" do + it_should_behave_like "load" do + let(:load_method) { :load_by_user } + let(:url) { "users/foobar/keys/test_key_name" } + end + end + + describe "load_by_client" do + it_should_behave_like "load" do + let(:load_method) { :load_by_client } + let(:url) { "clients/foobar/keys/test_key_name" } + end + end + + end #load + + describe "destroy" do + shared_examples_for "destroy key" do + context "when name is missing" do + it "should raise an MissingKeyAttribute" do + expect { Chef::Key.new("username", "user").destroy }.to raise_error(Chef::Exceptions::MissingKeyAttribute) + end + end + + before do + key.name "key_name" + end + context "when name is not missing" do + it "should delete the key via the API" do + expect(rest).to receive(:delete_rest).with(url).and_return({}) + key.destroy + end + end + end + + context "when destroying a user key" do + it_should_behave_like "destroy key" do + let(:url) { "users/#{key.actor}/keys/#{key.name}" } + let(:key) { user_key } + end + end + + context "when destroying a client key" do + it_should_behave_like "destroy key" do + let(:url) { "clients/#{client_key.actor}/keys/#{key.name}" } + let(:key) { client_key } + end + end + end + end # API Interactions +end diff --git a/spec/unit/knife/bootstrap_spec.rb b/spec/unit/knife/bootstrap_spec.rb index 848af11db5..0195e6d406 100644 --- a/spec/unit/knife/bootstrap_spec.rb +++ b/spec/unit/knife/bootstrap_spec.rb @@ -23,7 +23,7 @@ require 'net/ssh' describe Chef::Knife::Bootstrap do before do - allow(Chef::Platform).to receive(:windows?) { false } + allow(ChefConfig).to receive(:windows?) { false } end let(:knife) do Chef::Log.logger = Logger.new(StringIO.new) @@ -115,7 +115,7 @@ describe Chef::Knife::Bootstrap do end def configure_env_home - ENV['HOME'] = "/env/home" + allow(Chef::Util::PathHelper).to receive(:home).with(".chef", "bootstrap", "example.erb").and_yield(env_home_template_path) end def configure_gem_files @@ -123,15 +123,9 @@ describe Chef::Knife::Bootstrap do end before(:each) do - @original_home = ENV['HOME'] - ENV['HOME'] = nil expect(File).to receive(:exists?).with(bootstrap_template).and_return(false) end - after(:each) do - ENV['HOME'] = @original_home - end - context "when file is available everywhere" do before do configure_chef_config_dir @@ -161,7 +155,7 @@ describe Chef::Knife::Bootstrap do end end - context "when file is available in ENV['HOME']" do + context "when file is available in home directory" do before do configure_chef_config_dir configure_env_home @@ -180,10 +174,28 @@ describe Chef::Knife::Bootstrap do context "when file is available in Gem files" do before do configure_chef_config_dir + configure_env_home configure_gem_files expect(File).to receive(:exists?).with(builtin_template_path).and_return(false) expect(File).to receive(:exists?).with(chef_config_dir_template_path).and_return(false) + expect(File).to receive(:exists?).with(env_home_template_path).and_return(false) + expect(File).to receive(:exists?).with(gem_files_template_path).and_return(true) + end + + it "should load the template from Gem files" do + expect(knife.find_template).to eq(gem_files_template_path) + end + end + + context "when file is available in Gem files and home dir doesn't exist" do + before do + configure_chef_config_dir + configure_gem_files + allow(Chef::Util::PathHelper).to receive(:home).with(".chef", "bootstrap", "example.erb").and_return(nil) + + expect(File).to receive(:exists?).with(builtin_template_path).and_return(false) + expect(File).to receive(:exists?).with(chef_config_dir_template_path).and_return(false) expect(File).to receive(:exists?).with(gem_files_template_path).and_return(true) end @@ -519,6 +531,7 @@ describe Chef::Knife::Bootstrap do describe "when running the bootstrap" do let(:knife_ssh) do knife.name_args = ["foo.example.com"] + knife.config[:chef_node_name] = "foo.example.com" knife.config[:ssh_user] = "rooty" knife.config[:identity_file] = "~/.ssh/me.rsa" allow(knife).to receive(:render_template).and_return("") @@ -578,6 +591,12 @@ describe Chef::Knife::Bootstrap do expect(knife.chef_vault_handler).not_to receive(:run).with(node_name: knife.config[:chef_node_name]) knife.run end + + it "raises an exception if the config[:chef_node_name] is not present" do + knife.config[:chef_node_name] = nil + + expect { knife.run }.to raise_error(SystemExit) + end end context "when the validation key is not present" do @@ -592,6 +611,12 @@ describe Chef::Knife::Bootstrap do expect(knife.chef_vault_handler).to receive(:run).with(node_name: knife.config[:chef_node_name]) knife.run end + + it "raises an exception if the config[:chef_node_name] is not present" do + knife.config[:chef_node_name] = nil + + expect { knife.run }.to raise_error(SystemExit) + end end context "when the validation_key is nil" do diff --git a/spec/unit/knife/client_bulk_delete_spec.rb b/spec/unit/knife/client_bulk_delete_spec.rb index 45bb4dd16c..1a6317ac00 100644 --- a/spec/unit/knife/client_bulk_delete_spec.rb +++ b/spec/unit/knife/client_bulk_delete_spec.rb @@ -45,7 +45,7 @@ describe Chef::Knife::ClientBulkDelete do clients = Hash.new nonvalidator_client_names.each do |client_name| - client = Chef::ApiClient.new() + client = Chef::ApiClientV1.new() client.name(client_name) allow(client).to receive(:destroy).and_return(true) clients[client_name] = client @@ -59,7 +59,7 @@ describe Chef::Knife::ClientBulkDelete do clients = Hash.new validator_client_names.each do |validator_client_name| - validator_client = Chef::ApiClient.new() + validator_client = Chef::ApiClientV1.new() validator_client.name(validator_client_name) allow(validator_client).to receive(:validator).and_return(true) allow(validator_client).to receive(:destroy).and_return(true) @@ -75,7 +75,7 @@ describe Chef::Knife::ClientBulkDelete do } before(:each) do - allow(Chef::ApiClient).to receive(:list).and_return(clients) + allow(Chef::ApiClientV1).to receive(:list).and_return(clients) end describe "run" do @@ -89,7 +89,7 @@ describe Chef::Knife::ClientBulkDelete do describe "with any clients" do it "should get the list of the clients" do - expect(Chef::ApiClient).to receive(:list) + expect(Chef::ApiClientV1).to receive(:list) knife.run end diff --git a/spec/unit/knife/client_create_spec.rb b/spec/unit/knife/client_create_spec.rb index 10d386b5ff..a1dcc564e2 100644 --- a/spec/unit/knife/client_create_spec.rb +++ b/spec/unit/knife/client_create_spec.rb @@ -22,6 +22,8 @@ Chef::Knife::ClientCreate.load_deps describe Chef::Knife::ClientCreate do let(:stderr) { StringIO.new } + let(:stdout) { StringIO.new } + let(:default_client_hash) do { @@ -32,84 +34,153 @@ describe Chef::Knife::ClientCreate do end let(:client) do - c = double("Chef::ApiClient") - allow(c).to receive(:save).and_return({"private_key" => ""}) - allow(c).to receive(:to_s).and_return("client[adam]") - c + Chef::ApiClientV1.new end let(:knife) do k = Chef::Knife::ClientCreate.new - k.name_args = [ "adam" ] - k.ui.config[:disable_editing] = true + k.name_args = [] + allow(k).to receive(:client).and_return(client) + allow(k).to receive(:edit_data).with(client).and_return(client) allow(k.ui).to receive(:stderr).and_return(stderr) - allow(k.ui).to receive(:stdout).and_return(stderr) + allow(k.ui).to receive(:stdout).and_return(stdout) k end + before do + allow(client).to receive(:to_s).and_return("client[adam]") + allow(knife).to receive(:create_client).and_return(client) + end + before(:each) do Chef::Config[:node_name] = "webmonkey.example.com" end describe "run" do - it "should create and save the ApiClient" do - expect(Chef::ApiClient).to receive(:from_hash).and_return(client) - expect(client).to receive(:save) - knife.run + context "when nothing is passed" do + # from spec/support/shared/unit/knife_shared.rb + it_should_behave_like "mandatory field missing" do + let(:name_args) { [] } + let(:fieldname) { 'client name' } + end end - it "should print a message upon creation" do - expect(Chef::ApiClient).to receive(:from_hash).and_return(client) - expect(client).to receive(:save) - knife.run - expect(stderr.string).to match /Created client.*adam/i - end + context "when clientname is passed" do + before do + knife.name_args = ['adam'] + end - it "should set the Client name" do - expect(Chef::ApiClient).to receive(:from_hash).with(hash_including("name" => "adam")).and_return(client) - knife.run - end + context "when public_key and prevent_keygen are passed" do + before do + knife.config[:public_key] = "some_key" + knife.config[:prevent_keygen] = true + end + + it "prints the usage" do + expect(knife).to receive(:show_usage) + expect { knife.run }.to raise_error(SystemExit) + end + + it "prints a relevant error message" do + expect { knife.run }.to raise_error(SystemExit) + expect(stderr.string).to match /You cannot pass --public-key and --prevent-keygen/ + end + end - it "by default it is not an admin" do - expect(Chef::ApiClient).to receive(:from_hash).with(hash_including("admin" => false)).and_return(client) - knife.run - end + it "should create the ApiClient" do + expect(knife).to receive(:create_client) + knife.run + end - it "by default it is not a validator" do - expect(Chef::ApiClient).to receive(:from_hash).with(hash_including("validator" => false)).and_return(client) - knife.run - end + it "should print a message upon creation" do + expect(knife).to receive(:create_client) + knife.run + expect(stderr.string).to match /Created client.*adam/i + end - it "should allow you to edit the data" do - expect(knife).to receive(:edit_hash).with(default_client_hash).and_return(default_client_hash) - allow(Chef::ApiClient).to receive(:from_hash).and_return(client) - knife.run - end + it "should set the Client name" do + knife.run + expect(client.name).to eq("adam") + end - describe "with -f or --file" do - it "should write the private key to a file" do - knife.config[:file] = "/tmp/monkeypants" - allow_any_instance_of(Chef::ApiClient).to receive(:save).and_return({ 'private_key' => "woot" }) - filehandle = double("Filehandle") - expect(filehandle).to receive(:print).with('woot') - expect(File).to receive(:open).with("/tmp/monkeypants", "w").and_yield(filehandle) + it "by default it is not an admin" do knife.run + expect(client.admin).to be_falsey end - end - describe "with -a or --admin" do - it "should create an admin client" do - knife.config[:admin] = true - expect(Chef::ApiClient).to receive(:from_hash).with(hash_including("admin" => true)).and_return(client) + it "by default it is not a validator" do knife.run + expect(client.admin).to be_falsey end - end - describe "with --validator" do - it "should create an validator client" do - knife.config[:validator] = true - expect(Chef::ApiClient).to receive(:from_hash).with(hash_including("validator" => true)).and_return(client) + it "by default it should set create_key to true" do knife.run + expect(client.create_key).to be_truthy + end + + it "should allow you to edit the data" do + expect(knife).to receive(:edit_data).with(client).and_return(client) + knife.run + end + + describe "with -f or --file" do + before do + client.private_key "woot" + end + + it "should write the private key to a file" do + knife.config[:file] = "/tmp/monkeypants" + filehandle = double("Filehandle") + expect(filehandle).to receive(:print).with('woot') + expect(File).to receive(:open).with("/tmp/monkeypants", "w").and_yield(filehandle) + knife.run + end + end + + describe "with -a or --admin" do + before do + knife.config[:admin] = true + end + + it "should create an admin client" do + knife.run + expect(client.admin).to be_truthy + end + end + + describe "with -p or --public-key" do + before do + knife.config[:public_key] = 'some_key' + allow(File).to receive(:read).and_return('some_key') + allow(File).to receive(:expand_path) + end + + it "sets the public key" do + knife.run + expect(client.public_key).to eq('some_key') + end + end + + describe "with -k or --prevent-keygen" do + before do + knife.config[:prevent_keygen] = true + end + + it "does not set create_key" do + knife.run + expect(client.create_key).to be_falsey + end + end + + describe "with --validator" do + before do + knife.config[:validator] = true + end + + it "should create an validator client" do + knife.run + expect(client.validator).to be_truthy + end end end end diff --git a/spec/unit/knife/client_delete_spec.rb b/spec/unit/knife/client_delete_spec.rb index 0fb5e0bab7..619009979b 100644 --- a/spec/unit/knife/client_delete_spec.rb +++ b/spec/unit/knife/client_delete_spec.rb @@ -30,7 +30,7 @@ describe Chef::Knife::ClientDelete do describe 'run' do it 'should delete the client' do - expect(@knife).to receive(:delete_object).with(Chef::ApiClient, 'adam', 'client') + expect(@knife).to receive(:delete_object).with(Chef::ApiClientV1, 'adam', 'client') @knife.run end @@ -46,8 +46,8 @@ describe Chef::Knife::ClientDelete do before(:each) do allow(Chef::Knife::UI).to receive(:confirm).and_return(true) allow(@knife).to receive(:confirm).and_return(true) - @client = Chef::ApiClient.new - expect(Chef::ApiClient).to receive(:load).and_return(@client) + @client = Chef::ApiClientV1.new + expect(Chef::ApiClientV1).to receive(:load).and_return(@client) end it 'should delete non-validator client if --delete-validators is not set' do diff --git a/spec/unit/knife/client_edit_spec.rb b/spec/unit/knife/client_edit_spec.rb index c040c5e2f2..ad56d9212d 100644 --- a/spec/unit/knife/client_edit_spec.rb +++ b/spec/unit/knife/client_edit_spec.rb @@ -17,16 +17,29 @@ # require 'spec_helper' +require 'chef/api_client_v1' describe Chef::Knife::ClientEdit do before(:each) do @knife = Chef::Knife::ClientEdit.new @knife.name_args = [ 'adam' ] + @knife.config[:disable_editing] = true end describe 'run' do + let(:data) { + { + "name" => "adam", + "validator" => false, + "admin" => false, + "chef_type" => "client", + "create_key" => true + } + } + it 'should edit the client' do - expect(@knife).to receive(:edit_object).with(Chef::ApiClient, 'adam') + allow(Chef::ApiClientV1).to receive(:load).with('adam').and_return(data) + expect(@knife).to receive(:edit_data).with(data).and_return(data) @knife.run end diff --git a/spec/unit/knife/client_list_spec.rb b/spec/unit/knife/client_list_spec.rb index eff01da4e9..ce0fa4f5e8 100644 --- a/spec/unit/knife/client_list_spec.rb +++ b/spec/unit/knife/client_list_spec.rb @@ -26,7 +26,7 @@ describe Chef::Knife::ClientList do describe 'run' do it 'should list the clients' do - expect(Chef::ApiClient).to receive(:list) + expect(Chef::ApiClientV1).to receive(:list) expect(@knife).to receive(:format_list_for_display) @knife.run end diff --git a/spec/unit/knife/client_reregister_spec.rb b/spec/unit/knife/client_reregister_spec.rb index f1be4ed570..7e763242e4 100644 --- a/spec/unit/knife/client_reregister_spec.rb +++ b/spec/unit/knife/client_reregister_spec.rb @@ -41,7 +41,7 @@ describe Chef::Knife::ClientReregister do context 'when not configured for file output' do it 'reregisters the client and prints the key' do - expect(Chef::ApiClient).to receive(:reregister).with('adam').and_return(@client_mock) + expect(Chef::ApiClientV1).to receive(:reregister).with('adam').and_return(@client_mock) @knife.run expect(@stdout.string).to match( /foo_key/ ) end @@ -49,7 +49,7 @@ describe Chef::Knife::ClientReregister do context 'when configured for file output' do it 'should write the private key to a file' do - expect(Chef::ApiClient).to receive(:reregister).with('adam').and_return(@client_mock) + expect(Chef::ApiClientV1).to receive(:reregister).with('adam').and_return(@client_mock) @knife.config[:file] = '/tmp/monkeypants' filehandle = StringIO.new diff --git a/spec/unit/knife/client_show_spec.rb b/spec/unit/knife/client_show_spec.rb index 8404e8d019..73a876cee0 100644 --- a/spec/unit/knife/client_show_spec.rb +++ b/spec/unit/knife/client_show_spec.rb @@ -27,7 +27,7 @@ describe Chef::Knife::ClientShow do describe 'run' do it 'should list the client' do - expect(Chef::ApiClient).to receive(:load).with('adam').and_return(@client_mock) + expect(Chef::ApiClientV1).to receive(:load).with('adam').and_return(@client_mock) expect(@knife).to receive(:format_for_display).with(@client_mock) @knife.run end @@ -37,7 +37,7 @@ describe Chef::Knife::ClientShow do @stdout = StringIO.new allow(@knife.ui).to receive(:stdout).and_return(@stdout) fake_client_contents = {"foo"=>"bar", "baz"=>"qux"} - expect(Chef::ApiClient).to receive(:load).with('adam').and_return(fake_client_contents) + expect(Chef::ApiClientV1).to receive(:load).with('adam').and_return(fake_client_contents) @knife.run expect(@stdout.string).to eql("{\n \"foo\": \"bar\",\n \"baz\": \"qux\"\n}\n") end diff --git a/spec/unit/knife/core/custom_manifest_loader_spec.rb b/spec/unit/knife/core/custom_manifest_loader_spec.rb new file mode 100644 index 0000000000..1edbedd3c8 --- /dev/null +++ b/spec/unit/knife/core/custom_manifest_loader_spec.rb @@ -0,0 +1,41 @@ +# +# 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' + +describe Chef::Knife::SubcommandLoader::CustomManifestLoader do + let(:ec2_server_create_plugin) { "/usr/lib/ruby/gems/knife-ec2-0.5.12/lib/chef/knife/ec2_server_create.rb" } + let(:manifest_content) do + { "plugins" => { + "knife-ec2" => { + "paths" => [ + ec2_server_create_plugin + ] + } + } + } + end + let(:loader) do + Chef::Knife::SubcommandLoader::CustomManifestLoader.new(File.join(CHEF_SPEC_DATA, 'knife-site-subcommands'), + manifest_content) + end + + it "uses paths from the manifest instead of searching gems" do + expect(Gem::Specification).not_to receive(:latest_specs).and_call_original + expect(loader.subcommand_files).to include(ec2_server_create_plugin) + end +end diff --git a/spec/unit/knife/core/gem_glob_loader_spec.rb b/spec/unit/knife/core/gem_glob_loader_spec.rb new file mode 100644 index 0000000000..465eea2656 --- /dev/null +++ b/spec/unit/knife/core/gem_glob_loader_spec.rb @@ -0,0 +1,210 @@ +# +# 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' + +describe Chef::Knife::SubcommandLoader::GemGlobLoader do + let(:loader) { Chef::Knife::SubcommandLoader::GemGlobLoader.new(File.join(CHEF_SPEC_DATA, 'knife-site-subcommands')) } + let(:home) { File.join(CHEF_SPEC_DATA, 'knife-home') } + let(:plugin_dir) { File.join(home, '.chef', 'plugins', 'knife') } + + before do + allow(ChefConfig).to receive(:windows?) { false } + Chef::Util::PathHelper.class_variable_set(:@@home_dir, home) + end + + after do + Chef::Util::PathHelper.class_variable_set(:@@home_dir, nil) + end + + it "builds a list of the core subcommand file require paths" do + expect(loader.subcommand_files).not_to be_empty + loader.subcommand_files.each do |require_path| + expect(require_path).to match(/chef\/knife\/.*|plugins\/knife\/.*/) + end + end + + it "finds files installed via rubygems" do + expect(loader.find_subcommands_via_rubygems).to include('chef/knife/node_create') + loader.find_subcommands_via_rubygems.each {|rel_path, abs_path| expect(abs_path).to match(%r[chef/knife/.+])} + end + + it "finds files from latest version of installed gems" do + gems = [ double('knife-ec2-0.5.12') ] + gem_files = [ + '/usr/lib/ruby/gems/knife-ec2-0.5.12/lib/chef/knife/ec2_base.rb', + '/usr/lib/ruby/gems/knife-ec2-0.5.12/lib/chef/knife/ec2_otherstuff.rb' + ] + expect($LOAD_PATH).to receive(:map).and_return([]) + if Gem::Specification.respond_to? :latest_specs + expect(Gem::Specification).to receive(:latest_specs).with(true).and_return(gems) + expect(gems[0]).to receive(:matches_for_glob).with(/chef\/knife\/\*\.rb\{(.*),\.rb,(.*)\}/).and_return(gem_files) + else + expect(Gem.source_index).to receive(:latest_specs).with(true).and_return(gems) + expect(gems[0]).to receive(:require_paths).twice.and_return(['lib']) + expect(gems[0]).to receive(:full_gem_path).and_return('/usr/lib/ruby/gems/knife-ec2-0.5.12') + expect(Dir).to receive(:[]).with('/usr/lib/ruby/gems/knife-ec2-0.5.12/lib/chef/knife/*.rb').and_return(gem_files) + end + expect(loader).to receive(:find_subcommands_via_dirglob).and_return({}) + expect(loader.subcommand_files.select { |file| file =~ /knife-ec2/ }.sort).to eq(gem_files) + end + + it "finds files using a dirglob when rubygems is not available" do + expect(loader.find_subcommands_via_dirglob).to include('chef/knife/node_create') + loader.find_subcommands_via_dirglob.each {|rel_path, abs_path| expect(abs_path).to match(%r[chef/knife/.+])} + end + + it "finds user-specific subcommands in the user's ~/.chef directory" do + expected_command = File.join(home, '.chef', 'plugins', 'knife', 'example_home_subcommand.rb') + expect(loader.site_subcommands).to include(expected_command) + end + + it "finds repo specific subcommands by searching for a .chef directory" do + expected_command = File.join(CHEF_SPEC_DATA, 'knife-site-subcommands', 'plugins', 'knife', 'example_subcommand.rb') + expect(loader.site_subcommands).to include(expected_command) + end + + # https://github.com/opscode/chef-dk/issues/227 + # + # `knife` in ChefDK isn't from a gem install, it's directly run from a clone + # of the source, but there can be one or more versions of chef also installed + # as a gem. If the gem install contains a command that doesn't exist in the + # source tree of the "primary" chef install, it can be loaded and cause an + # error. We also want to ensure that we only load builtin commands from the + # "primary" chef install. + context "when a different version of chef is also installed as a gem" do + + let(:all_found_commands) do + [ + "/opt/chefdk/embedded/apps/chef/lib/chef/knife/bootstrap.rb", + "/opt/chefdk/embedded/apps/chef/lib/chef/knife/client_bulk_delete.rb", + "/opt/chefdk/embedded/apps/chef/lib/chef/knife/client_create.rb", + + # We use the fake version 1.0.0 because that version doesn't exist, + # which ensures it won't ever equal "chef-#{Chef::VERSION}" + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-1.0.0/lib/chef/knife/bootstrap.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-1.0.0/lib/chef/knife/client_bulk_delete.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-1.0.0/lib/chef/knife/client_create.rb", + + # Test that we don't accept a version number that is different only in + # trailing characters, e.g. we are running Chef 12.0.0 but there is a + # Chef 12.0.0.rc.0 gem also: + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}.rc.0/lib/chef/knife/thing.rb", + + # Test that we ignore the platform suffix when checking for different + # gem versions. + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-x86-mingw32/lib/chef/knife/valid.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-i386-mingw64/lib/chef/knife/valid-too.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-mswin32/lib/chef/knife/also-valid.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-universal-mingw32/lib/chef/knife/universal-is-valid.rb", + # ...but don't ignore the .rc / .dev parts in the case when we have + # platform suffixes + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}.rc.0-x86-mingw32/lib/chef/knife/invalid.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}.dev-mswin32/lib/chef/knife/invalid-too.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}.dev.0-x86-mingw64/lib/chef/knife/still-invalid.rb", + + # This command is "extra" compared to what's in the embedded/apps/chef install: + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-1.0.0/lib/chef/knife/data_bag_secret_options.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-vault-2.2.4/lib/chef/knife/decrypt.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/knife-spork-1.4.1/lib/chef/knife/spork-bump.rb", + + # These are fake commands that have names designed to test that the + # regex is strict enough + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-foo-#{Chef::VERSION}/lib/chef/knife/chef-foo.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/foo-chef-#{Chef::VERSION}/lib/chef/knife/foo-chef.rb", + + # In a real scenario, we'd use rubygems APIs to only select the most + # recent gem, but for this test we want to check that we're doing the + # right thing both when the plugin version matches and does not match + # the current chef version. Looking at + # `SubcommandLoader::MATCHES_THIS_CHEF_GEM` and + # `SubcommandLoader::MATCHES_CHEF_GEM` should make it clear why we want + # to test these two cases. + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-bar-1.0.0/lib/chef/knife/chef-bar.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/bar-chef-1.0.0/lib/chef/knife/bar-chef.rb" + ] + end + + let(:expected_valid_commands) do + [ + "/opt/chefdk/embedded/apps/chef/lib/chef/knife/bootstrap.rb", + "/opt/chefdk/embedded/apps/chef/lib/chef/knife/client_bulk_delete.rb", + "/opt/chefdk/embedded/apps/chef/lib/chef/knife/client_create.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-x86-mingw32/lib/chef/knife/valid.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-i386-mingw64/lib/chef/knife/valid-too.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-mswin32/lib/chef/knife/also-valid.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-universal-mingw32/lib/chef/knife/universal-is-valid.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-vault-2.2.4/lib/chef/knife/decrypt.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/knife-spork-1.4.1/lib/chef/knife/spork-bump.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-foo-#{Chef::VERSION}/lib/chef/knife/chef-foo.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/foo-chef-#{Chef::VERSION}/lib/chef/knife/foo-chef.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-bar-1.0.0/lib/chef/knife/chef-bar.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/bar-chef-1.0.0/lib/chef/knife/bar-chef.rb" + ] + end + + before do + expect(loader).to receive(:find_files_latest_gems).with("chef/knife/*.rb").and_return(all_found_commands) + expect(loader).to receive(:find_subcommands_via_dirglob).and_return({}) + end + + it "ignores commands from the non-matching gem install" do + expect(loader.find_subcommands_via_rubygems.values).to eq(expected_valid_commands) + end + + end + + describe "finding 3rd party plugins" do + let(:env_home) { "/home/alice" } + let(:manifest_path) { env_home + "/.chef/plugin_manifest.json" } + + before do + env_dup = ENV.to_hash + allow(ENV).to receive(:[]) { |key| env_dup[key] } + allow(ENV).to receive(:[]).with("HOME").and_return(env_home) + end + + + it "searches rubygems for plugins" do + if Gem::Specification.respond_to?(:latest_specs) + expect(Gem::Specification).to receive(:latest_specs).and_call_original + else + expect(Gem.source_index).to receive(:latest_specs).and_call_original + end + loader.subcommand_files.each do |require_path| + expect(require_path).to match(/chef\/knife\/.*|plugins\/knife\/.*/) + end + end + + context "and HOME environment variable is not set" do + before do + allow(ENV).to receive(:[]).with("HOME").and_return(nil) + end + + it "searches rubygems for plugins" do + if Gem::Specification.respond_to?(:latest_specs) + expect(Gem::Specification).to receive(:latest_specs).and_call_original + else + expect(Gem.source_index).to receive(:latest_specs).and_call_original + end + loader.subcommand_files.each do |require_path| + expect(require_path).to match(/chef\/knife\/.*|plugins\/knife\/.*/) + end + end + end + end +end diff --git a/spec/unit/knife/core/hashed_command_loader_spec.rb b/spec/unit/knife/core/hashed_command_loader_spec.rb new file mode 100644 index 0000000000..00e7ba377b --- /dev/null +++ b/spec/unit/knife/core/hashed_command_loader_spec.rb @@ -0,0 +1,93 @@ +# +# 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' + +describe Chef::Knife::SubcommandLoader::HashedCommandLoader do + before do + allow(ChefConfig).to receive(:windows?) { false } + end + + let(:plugin_manifest) { + { + "_autogenerated_command_paths" => { + "plugins_paths" => { + "cool_a" => ["/file/for/plugin/a"], + "cooler_b" => ["/file/for/plugin/b"] + }, + "plugins_by_category" => { + "cool" => [ + "cool_a" + ], + "cooler" => [ + "cooler_b" + ] + } + } + } + } + + let(:loader) { Chef::Knife::SubcommandLoader::HashedCommandLoader.new( + File.join(CHEF_SPEC_DATA, 'knife-site-subcommands'), + plugin_manifest)} + + describe "#list_commands" do + it "lists all commands by category when no argument is given" do + expect(loader.list_commands).to eq({"cool" => ["cool_a"], "cooler" => ["cooler_b"]}) + end + + it "lists only commands in the given category when a category is given" do + expect(loader.list_commands("cool")).to eq({"cool" => ["cool_a"]}) + end + end + + describe "#subcommand_files" do + it "lists all the files" do + expect(loader.subcommand_files).to eq(["/file/for/plugin/a", "/file/for/plugin/b"]) + end + end + + describe "#load_commands" do + before do + allow(Kernel).to receive(:load).and_return(true) + end + + it "returns false for non-existant commands" do + expect(loader.load_command(["nothere"])).to eq(false) + end + + it "loads the correct file and returns true if the command exists" do + allow(File).to receive(:exists?).and_return(true) + expect(Kernel).to receive(:load).with("/file/for/plugin/a").and_return(true) + expect(loader.load_command(["cool_a"])).to eq(true) + end + end + + describe "#subcommand_for_args" do + it "returns the subcommands for an exact match" do + expect(loader.subcommand_for_args(["cooler_b"])).to eq("cooler_b") + end + + it "finds the right subcommand even when _'s are elided" do + expect(loader.subcommand_for_args(["cooler", "b"])).to eq("cooler_b") + end + + it "returns nil if the the subcommand isn't in our manifest" do + expect(loader.subcommand_for_args(["cooler c"])).to eq(nil) + end + end +end diff --git a/spec/unit/knife/core/subcommand_loader_spec.rb b/spec/unit/knife/core/subcommand_loader_spec.rb index df42cff2ea..2386465c75 100644 --- a/spec/unit/knife/core/subcommand_loader_spec.rb +++ b/spec/unit/knife/core/subcommand_loader_spec.rb @@ -1,6 +1,5 @@ # -# Author:: Daniel DeLeo (<dan@opscode.com>) -# Copyright:: Copyright (c) 2011 Opscode, Inc. +# Copyright:: Copyright (c) 2015 Chef Software, Inc # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,206 +18,47 @@ require 'spec_helper' describe Chef::Knife::SubcommandLoader do - before do - allow(Chef::Platform).to receive(:windows?) { false } - @home = File.join(CHEF_SPEC_DATA, 'knife-home') - @env = {'HOME' => @home} - @loader = Chef::Knife::SubcommandLoader.new(File.join(CHEF_SPEC_DATA, 'knife-site-subcommands'), @env) - end - - it "builds a list of the core subcommand file require paths" do - expect(@loader.subcommand_files).not_to be_empty - @loader.subcommand_files.each do |require_path| - expect(require_path).to match(/chef\/knife\/.*|plugins\/knife\/.*/) - end - end - - it "finds files installed via rubygems" do - expect(@loader.find_subcommands_via_rubygems).to include('chef/knife/node_create') - @loader.find_subcommands_via_rubygems.each {|rel_path, abs_path| expect(abs_path).to match(%r[chef/knife/.+])} - end - - it "finds files from latest version of installed gems" do - gems = [ double('knife-ec2-0.5.12') ] - gem_files = [ - '/usr/lib/ruby/gems/knife-ec2-0.5.12/lib/chef/knife/ec2_base.rb', - '/usr/lib/ruby/gems/knife-ec2-0.5.12/lib/chef/knife/ec2_otherstuff.rb' - ] - expect($LOAD_PATH).to receive(:map).and_return([]) - if Gem::Specification.respond_to? :latest_specs - expect(Gem::Specification).to receive(:latest_specs).with(true).and_return(gems) - expect(gems[0]).to receive(:matches_for_glob).with(/chef\/knife\/\*\.rb\{(.*),\.rb,(.*)\}/).and_return(gem_files) - else - expect(Gem.source_index).to receive(:latest_specs).with(true).and_return(gems) - expect(gems[0]).to receive(:require_paths).twice.and_return(['lib']) - expect(gems[0]).to receive(:full_gem_path).and_return('/usr/lib/ruby/gems/knife-ec2-0.5.12') - expect(Dir).to receive(:[]).with('/usr/lib/ruby/gems/knife-ec2-0.5.12/lib/chef/knife/*.rb').and_return(gem_files) - end - expect(@loader).to receive(:find_subcommands_via_dirglob).and_return({}) - expect(@loader.find_subcommands_via_rubygems.values.select { |file| file =~ /knife-ec2/ }.sort).to eq(gem_files) - end + let(:loader) { Chef::Knife::SubcommandLoader.new(File.join(CHEF_SPEC_DATA, 'knife-site-subcommands')) } + let(:home) { File.join(CHEF_SPEC_DATA, 'knife-home') } + let(:plugin_dir) { File.join(home, '.chef', 'plugins', 'knife') } - it "finds files using a dirglob when rubygems is not available" do - expect(@loader.find_subcommands_via_dirglob).to include('chef/knife/node_create') - @loader.find_subcommands_via_dirglob.each {|rel_path, abs_path| expect(abs_path).to match(%r[chef/knife/.+])} - end - - it "finds user-specific subcommands in the user's ~/.chef directory" do - expected_command = File.join(@home, '.chef', 'plugins', 'knife', 'example_home_subcommand.rb') - expect(@loader.site_subcommands).to include(expected_command) + before do + allow(ChefConfig).to receive(:windows?) { false } + Chef::Util::PathHelper.class_variable_set(:@@home_dir, home) end - it "finds repo specific subcommands by searching for a .chef directory" do - expected_command = File.join(CHEF_SPEC_DATA, 'knife-site-subcommands', 'plugins', 'knife', 'example_subcommand.rb') - expect(@loader.site_subcommands).to include(expected_command) + after do + Chef::Util::PathHelper.class_variable_set(:@@home_dir, nil) end - # https://github.com/opscode/chef-dk/issues/227 - # - # `knife` in ChefDK isn't from a gem install, it's directly run from a clone - # of the source, but there can be one or more versions of chef also installed - # as a gem. If the gem install contains a command that doesn't exist in the - # source tree of the "primary" chef install, it can be loaded and cause an - # error. We also want to ensure that we only load builtin commands from the - # "primary" chef install. - context "when a different version of chef is also installed as a gem" do - - let(:all_found_commands) do - [ - "/opt/chefdk/embedded/apps/chef/lib/chef/knife/bootstrap.rb", - "/opt/chefdk/embedded/apps/chef/lib/chef/knife/client_bulk_delete.rb", - "/opt/chefdk/embedded/apps/chef/lib/chef/knife/client_create.rb", - - # We use the fake version 1.0.0 because that version doesn't exist, - # which ensures it won't ever equal "chef-#{Chef::VERSION}" - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-1.0.0/lib/chef/knife/bootstrap.rb", - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-1.0.0/lib/chef/knife/client_bulk_delete.rb", - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-1.0.0/lib/chef/knife/client_create.rb", - - # Test that we don't accept a version number that is different only in - # trailing characters, e.g. we are running Chef 12.0.0 but there is a - # Chef 12.0.0.rc.0 gem also: - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}.rc.0/lib/chef/knife/thing.rb", - - # This command is "extra" compared to what's in the embedded/apps/chef install: - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-1.0.0/lib/chef/knife/data_bag_secret_options.rb", - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-vault-2.2.4/lib/chef/knife/decrypt.rb", - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/knife-spork-1.4.1/lib/chef/knife/spork-bump.rb", - - # These are fake commands that have names designed to test that the - # regex is strict enough - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-foo-#{Chef::VERSION}/lib/chef/knife/chef-foo.rb", - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/foo-chef-#{Chef::VERSION}/lib/chef/knife/foo-chef.rb", - - # In a real scenario, we'd use rubygems APIs to only select the most - # recent gem, but for this test we want to check that we're doing the - # right thing both when the plugin version matches and does not match - # the current chef version. Looking at - # `SubcommandLoader::MATCHES_THIS_CHEF_GEM` and - # `SubcommandLoader::MATCHES_CHEF_GEM` should make it clear why we want - # to test these two cases. - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-bar-1.0.0/lib/chef/knife/chef-bar.rb", - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/bar-chef-1.0.0/lib/chef/knife/bar-chef.rb" - ] - end + let(:config_dir) { File.join(CHEF_SPEC_DATA, 'knife-site-subcommands') } - let(:expected_valid_commands) do - [ - "/opt/chefdk/embedded/apps/chef/lib/chef/knife/bootstrap.rb", - "/opt/chefdk/embedded/apps/chef/lib/chef/knife/client_bulk_delete.rb", - "/opt/chefdk/embedded/apps/chef/lib/chef/knife/client_create.rb", - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-vault-2.2.4/lib/chef/knife/decrypt.rb", - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/knife-spork-1.4.1/lib/chef/knife/spork-bump.rb", - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-foo-#{Chef::VERSION}/lib/chef/knife/chef-foo.rb", - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/foo-chef-#{Chef::VERSION}/lib/chef/knife/foo-chef.rb", - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-bar-1.0.0/lib/chef/knife/chef-bar.rb", - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/bar-chef-1.0.0/lib/chef/knife/bar-chef.rb" - ] - end - - before do - expect(@loader).to receive(:find_files_latest_gems).with("chef/knife/*.rb").and_return(all_found_commands) - expect(@loader).to receive(:find_subcommands_via_dirglob).and_return({}) - end - - it "ignores commands from the non-matching gem install" do - expect(@loader.find_subcommands_via_rubygems.values).to eq(expected_valid_commands) - end - - end - - describe "finding 3rd party plugins" do - let(:env_home) { "/home/alice" } - let(:manifest_path) { env_home + "/.chef/plugin_manifest.json" } - - before do - env_dup = ENV.to_hash - allow(ENV).to receive(:[]) { |key| env_dup[key] } - allow(ENV).to receive(:[]).with("HOME").and_return(env_home) - end - - context "when there is not a ~/.chef/plugin_manifest.json file" do + describe "#for_config" do + context "when ~/.chef/plugin_manifest.json exists" do before do - allow(File).to receive(:exist?).with(manifest_path).and_return(false) + allow(File).to receive(:exist?).with(File.join(home, '.chef', 'plugin_manifest.json')).and_return(true) end - it "searches rubygems for plugins" do - if Gem::Specification.respond_to?(:latest_specs) - expect(Gem::Specification).to receive(:latest_specs).and_call_original - else - expect(Gem.source_index).to receive(:latest_specs).and_call_original - end - @loader.subcommand_files.each do |require_path| - expect(require_path).to match(/chef\/knife\/.*|plugins\/knife\/.*/) - end + it "creates a HashedCommandLoader with the manifest has _autogenerated_command_paths" do + allow(File).to receive(:read).with(File.join(home, '.chef', 'plugin_manifest.json')).and_return("{ \"_autogenerated_command_paths\": {}}") + expect(Chef::Knife::SubcommandLoader.for_config(config_dir)).to be_a Chef::Knife::SubcommandLoader::HashedCommandLoader end - context "and HOME environment variable is not set" do - before do - allow(ENV).to receive(:[]).with("HOME").and_return(nil) - end - - it "searches rubygems for plugins" do - if Gem::Specification.respond_to?(:latest_specs) - expect(Gem::Specification).to receive(:latest_specs).and_call_original - else - expect(Gem.source_index).to receive(:latest_specs).and_call_original - end - @loader.subcommand_files.each do |require_path| - expect(require_path).to match(/chef\/knife\/.*|plugins\/knife\/.*/) - end - end + it "creates a CustomManifestLoader with then manifest has a key other than _autogenerated_command_paths" do + Chef::Config[:treat_deprecation_warnings_as_errors] = false + allow(File).to receive(:read).with(File.join(home, '.chef', 'plugin_manifest.json')).and_return("{ \"plugins\": {}}") + expect(Chef::Knife::SubcommandLoader.for_config(config_dir)).to be_a Chef::Knife::SubcommandLoader::CustomManifestLoader end - end - context "when there is a ~/.chef/plugin_manifest.json file" do - let(:ec2_server_create_plugin) { "/usr/lib/ruby/gems/knife-ec2-0.5.12/lib/chef/knife/ec2_server_create.rb" } - - let(:manifest_content) do - { "plugins" => { - "knife-ec2" => { - "paths" => [ - ec2_server_create_plugin - ] - } - } - } - end - - let(:manifest_json) { Chef::JSONCompat.to_json(manifest_content) } - + context "when ~/.chef/plugin_manifest.json does not exist" do before do - allow(File).to receive(:exist?).with(manifest_path).and_return(true) - allow(File).to receive(:read).with(manifest_path).and_return(manifest_json) + allow(File).to receive(:exist?).with(File.join(home, '.chef', 'plugin_manifest.json')).and_return(false) end - it "uses paths from the manifest instead of searching gems" do - expect(Gem::Specification).not_to receive(:latest_specs).and_call_original - expect(@loader.subcommand_files).to include(ec2_server_create_plugin) + it "creates a GemGlobLoader" do + expect(Chef::Knife::SubcommandLoader.for_config(config_dir)).to be_a Chef::Knife::SubcommandLoader::GemGlobLoader end - end end - end diff --git a/spec/unit/knife/core/ui_spec.rb b/spec/unit/knife/core/ui_spec.rb index ac42ad6dd6..ab420518a3 100644 --- a/spec/unit/knife/core/ui_spec.rb +++ b/spec/unit/knife/core/ui_spec.rb @@ -368,6 +368,20 @@ EOM @ui.config[:attribute] = "keys.keys" expect(@ui.format_for_display(input)).to eq({ "sample-data-bag-item" => { "keys.keys" => "values" } }) end + + it "should return the name attribute" do + allow_any_instance_of(Chef::Node).to receive(:name).and_return("chef.localdomain") + input = Chef::Node.new + @ui.config[:attribute] = "name" + expect(@ui.format_for_display(input)).to eq( {"chef.localdomain"=>{"name"=>"chef.localdomain"} }) + end + + it "returns nil when given an attribute path that isn't a name or attribute" do + input = { "keys" => {"keys" => "values"}, "hi" => "ho", "id" => "sample-data-bag-item" } + non_existing_path = "nope.nada.nothingtoseehere" + @ui.config[:attribute] = non_existing_path + expect(@ui.format_for_display(input)).to eq({ "sample-data-bag-item" => { non_existing_path => nil } }) + end end describe "with --run-list passed" do @@ -420,7 +434,7 @@ EOM before(:each) do stdout = double('StringIO', :tty? => true) allow(@ui).to receive(:stdout).and_return(stdout) - allow(Chef::Platform).to receive(:windows?) { true } + allow(ChefConfig).to receive(:windows?) { true } Chef::Config.reset end diff --git a/spec/unit/knife/data_bag_from_file_spec.rb b/spec/unit/knife/data_bag_from_file_spec.rb index 3882bff349..8b6502145c 100644 --- a/spec/unit/knife/data_bag_from_file_spec.rb +++ b/spec/unit/knife/data_bag_from_file_spec.rb @@ -26,7 +26,7 @@ Chef::Knife::DataBagFromFile.load_deps describe Chef::Knife::DataBagFromFile do before :each do - allow(Chef::Platform).to receive(:windows?) { false } + allow(ChefConfig).to receive(:windows?) { false } Chef::Config[:node_name] = "webmonkey.example.com" FileUtils.mkdir_p([db_folder, db_folder2]) db_file.write(Chef::JSONCompat.to_json(plain_data)) diff --git a/spec/unit/knife/environment_from_file_spec.rb b/spec/unit/knife/environment_from_file_spec.rb index d150e5ee64..11ad23c919 100644 --- a/spec/unit/knife/environment_from_file_spec.rb +++ b/spec/unit/knife/environment_from_file_spec.rb @@ -23,7 +23,7 @@ Chef::Knife::EnvironmentFromFile.load_deps describe Chef::Knife::EnvironmentFromFile do before(:each) do - allow(Chef::Platform).to receive(:windows?) { false } + allow(ChefConfig).to receive(:windows?) { false } @knife = Chef::Knife::EnvironmentFromFile.new @stdout = StringIO.new allow(@knife.ui).to receive(:stdout).and_return(@stdout) diff --git a/spec/unit/knife/key_create_spec.rb b/spec/unit/knife/key_create_spec.rb new file mode 100644 index 0000000000..5998e10274 --- /dev/null +++ b/spec/unit/knife/key_create_spec.rb @@ -0,0 +1,224 @@ +# +# Author:: Tyler Cloke (<tyler@chef.io>) +# 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/knife/user_key_create' +require 'chef/knife/client_key_create' +require 'chef/knife/key_create' +require 'chef/key' + +describe "key create commands that inherit knife" do + shared_examples_for "a key create command" do + let(:stderr) { StringIO.new } + let(:params) { [] } + let(:service_object) { instance_double(Chef::Knife::KeyCreate) } + let(:command) do + c = described_class.new([]) + c.ui.config[:disable_editing] = true + allow(c.ui).to receive(:stderr).and_return(stderr) + allow(c.ui).to receive(:stdout).and_return(stderr) + allow(c).to receive(:show_usage) + c + end + + context "after apply_params! is called with valid args" do + let(:params) { ["charmander"] } + before do + command.apply_params!(params) + end + + context "when the service object is called" do + it "creates a new instance of Chef::Knife::KeyCreate with the correct args" do + expect(Chef::Knife::KeyCreate).to receive(:new). + with("charmander", command.actor_field_name, command.ui, command.config). + and_return(service_object) + command.service_object + end + end # when the service object is called + end # after apply_params! is called with valid args + end # a key create command + + describe Chef::Knife::UserKeyCreate do + it_should_behave_like "a key create command" + # defined in key_helper.rb + it_should_behave_like "a knife key command" do + let(:service_object) { instance_double(Chef::Knife::KeyCreate) } + let(:params) { ["charmander"] } + end + end + + describe Chef::Knife::ClientKeyCreate do + it_should_behave_like "a key create command" + # defined in key_helper.rb + it_should_behave_like "a knife key command" do + let(:service_object) { instance_double(Chef::Knife::KeyCreate) } + let(:params) { ["charmander"] } + end + end +end + +describe Chef::Knife::KeyCreate do + let(:public_key) { + "-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvPo+oNPB7uuNkws0fC02 +KxSwdyqPLu0fhI1pOweNKAZeEIiEz2PkybathHWy8snSXGNxsITkf3eyvIIKa8OZ +WrlqpI3yv/5DOP8HTMCxnFuMJQtDwMcevlqebX4bCxcByuBpNYDcAHjjfLGSfMjn +E5lZpgYWwnpic4kSjYcL9ORK9nYvlWV9P/kCYmRhIjB4AhtpWRiOfY/TKi3P2LxT +IjSmiN/ihHtlhV/VSnBJ5PzT/lRknlrJ4kACoz7Pq9jv+aAx5ft/xE9yDa2DYs0q +Tfuc9dUYsFjptWYrV6pfEQ+bgo1OGBXORBFcFL+2D7u9JYquKrMgosznHoEkQNLo +0wIDAQAB +-----END PUBLIC KEY-----" + } + let(:config) { Hash.new } + let(:actor) { "charmander" } + let(:ui) { instance_double("Chef::Knife::UI") } + + shared_examples_for "key create run command" do + let(:key_create_object) { + described_class.new(actor, actor_field_name, ui, config) + } + + context "when public_key and key_name weren't passed" do + it "raises a Chef::Exceptions::KeyCommandInputError with the proper error message" do + expect{ key_create_object.run }.to raise_error(Chef::Exceptions::KeyCommandInputError, key_create_object.public_key_or_key_name_error_msg) + end + end + + context "when the command is run" do + let(:expected_hash) { + { + actor_field_name => "charmander" + } + } + + before do + allow(File).to receive(:read).and_return(public_key) + allow(File).to receive(:expand_path) + + allow(key_create_object).to receive(:output_private_key_to_file) + allow(key_create_object).to receive(:display_private_key) + allow(key_create_object).to receive(:edit_data).and_return(expected_hash) + allow(key_create_object).to receive(:create_key_from_hash).and_return(Chef::Key.from_hash(expected_hash)) + allow(key_create_object).to receive(:display_info) + end + + context "when a valid hash is passed" do + let(:key_name) { "charmander-key" } + let(:valid_expiration_date) { "2020-12-24T21:00:00Z" } + let(:expected_hash) { + { + actor_field_name => "charmander", + "public_key" => public_key, + "expiration_date" => valid_expiration_date, + "key_name" => key_name + } + } + before do + key_create_object.config[:public_key] = "public_key_path" + key_create_object.config[:expiration_Date] = valid_expiration_date, + key_create_object.config[:key_name] = key_name + end + + it "creates the proper hash" do + expect(key_create_object).to receive(:create_key_from_hash).with(expected_hash) + key_create_object.run + end + end + + context "when public_key is passed" do + let(:expected_hash) { + { + actor_field_name => "charmander", + "public_key" => public_key + } + } + before do + key_create_object.config[:public_key] = "public_key_path" + end + + it "calls File.expand_path with the public_key input" do + expect(File).to receive(:expand_path).with("public_key_path") + key_create_object.run + end + end # when public_key is passed + + context "when public_key isn't passed and key_name is" do + let(:expected_hash) { + { + actor_field_name => "charmander", + "name" => "charmander-key", + "create_key" => true + } + } + before do + key_create_object.config[:key_name] = "charmander-key" + end + + it "should set create_key to true" do + expect(key_create_object).to receive(:create_key_from_hash).with(expected_hash) + key_create_object.run + end + end + + context "when the server returns a private key" do + let(:expected_hash) { + { + actor_field_name => "charmander", + "public_key" => public_key, + "private_key" => "super_private" + } + } + + before do + key_create_object.config[:public_key] = "public_key_path" + end + + context "when file is not passed" do + it "calls display_private_key with the private_key" do + expect(key_create_object).to receive(:display_private_key).with("super_private") + key_create_object.run + end + end + + context "when file is passed" do + before do + key_create_object.config[:file] = "/fake/file" + end + + it "calls output_private_key_to_file with the private_key" do + expect(key_create_object).to receive(:output_private_key_to_file).with("super_private") + key_create_object.run + end + end + end # when the server returns a private key + end # when the command is run + end #key create run command" + + context "when actor_field_name is 'user'" do + it_should_behave_like "key create run command" do + let(:actor_field_name) { "user" } + end + end + + context "when actor_field_name is 'client'" do + it_should_behave_like "key create run command" do + let(:actor_field_name) { "client" } + end + end +end + diff --git a/spec/unit/knife/key_delete_spec.rb b/spec/unit/knife/key_delete_spec.rb new file mode 100644 index 0000000000..1d4b9f825f --- /dev/null +++ b/spec/unit/knife/key_delete_spec.rb @@ -0,0 +1,135 @@ +# +# Author:: Tyler Cloke (<tyler@chef.io>) +# 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/knife/user_key_delete' +require 'chef/knife/client_key_delete' +require 'chef/knife/key_delete' +require 'chef/key' + +describe "key delete commands that inherit knife" do + shared_examples_for "a key delete command" do + let(:stderr) { StringIO.new } + let(:params) { [] } + let(:service_object) { instance_double(Chef::Knife::KeyDelete) } + let(:command) do + c = described_class.new([]) + c.ui.config[:disable_editing] = true + allow(c.ui).to receive(:stderr).and_return(stderr) + allow(c.ui).to receive(:stdout).and_return(stderr) + allow(c).to receive(:show_usage) + c + end + + context "after apply_params! is called with valid args" do + let(:params) { ["charmander", "charmander-key"] } + before do + command.apply_params!(params) + end + + context "when the service object is called" do + it "creates a new instance of Chef::Knife::KeyDelete with the correct args" do + expect(Chef::Knife::KeyDelete).to receive(:new). + with("charmander-key", "charmander", command.actor_field_name, command.ui). + and_return(service_object) + command.service_object + end + end # when the service object is called + end # after apply_params! is called with valid args + end # a key delete command + + describe Chef::Knife::UserKeyDelete do + it_should_behave_like "a key delete command" + # defined in key_helpers.rb + it_should_behave_like "a knife key command with a keyname as the second arg" + it_should_behave_like "a knife key command" do + let(:service_object) { instance_double(Chef::Knife::KeyDelete) } + let(:params) { ["charmander", "charmander-key"] } + end + end + + describe Chef::Knife::ClientKeyDelete do + it_should_behave_like "a key delete command" + # defined in key_helpers.rb + it_should_behave_like "a knife key command with a keyname as the second arg" + it_should_behave_like "a knife key command" do + let(:service_object) { instance_double(Chef::Knife::KeyDelete) } + let(:params) { ["charmander", "charmander-key"] } + end + end +end + +describe Chef::Knife::KeyDelete do + let(:actor) { "charmander" } + let(:keyname) { "charmander-key" } + let(:ui) { instance_double("Chef::Knife::UI") } + + shared_examples_for "key delete run command" do + let(:key_delete_object) { + described_class.new(keyname, actor, actor_field_name, ui) + } + + before do + allow_any_instance_of(Chef::Key).to receive(:destroy) + allow(key_delete_object).to receive(:print_destroyed) + allow(key_delete_object).to receive(:confirm!) + end + + context "when the command is run" do + it "calls Chef::Key.new with the proper input" do + expect(Chef::Key).to receive(:new).with(actor, actor_field_name).and_call_original + key_delete_object.run + end + + it "calls name on the Chef::Key instance with the proper input" do + expect_any_instance_of(Chef::Key).to receive(:name).with(keyname) + key_delete_object.run + end + + it "calls destroy on the Chef::Key instance" do + expect_any_instance_of(Chef::Key).to receive(:destroy).once + key_delete_object.run + end + + it "calls confirm!" do + expect(key_delete_object).to receive(:confirm!) + key_delete_object.run + end + + it "calls print_destroyed" do + expect(key_delete_object).to receive(:print_destroyed) + key_delete_object.run + end + end # when the command is run + + + end # key delete run command + + context "when actor_field_name is 'user'" do + it_should_behave_like "key delete run command" do + let(:actor_field_name) { "user" } + end + end + + context "when actor_field_name is 'client'" do + it_should_behave_like "key delete run command" do + let(:actor_field_name) { "client" } + end + end +end + diff --git a/spec/unit/knife/key_edit_spec.rb b/spec/unit/knife/key_edit_spec.rb new file mode 100644 index 0000000000..538b91de2d --- /dev/null +++ b/spec/unit/knife/key_edit_spec.rb @@ -0,0 +1,267 @@ +# +# Author:: Tyler Cloke (<tyler@chef.io>) +# 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/knife/user_key_edit' +require 'chef/knife/client_key_edit' +require 'chef/knife/key_edit' +require 'chef/key' + +describe "key edit commands that inherit knife" do + shared_examples_for "a key edit command" do + let(:stderr) { StringIO.new } + let(:params) { [] } + let(:service_object) { instance_double(Chef::Knife::KeyEdit) } + let(:command) do + c = described_class.new([]) + c.ui.config[:disable_editing] = true + allow(c.ui).to receive(:stderr).and_return(stderr) + allow(c.ui).to receive(:stdout).and_return(stderr) + allow(c).to receive(:show_usage) + c + end + + context "after apply_params! is called with valid args" do + let(:params) { ["charmander", "charmander-key"] } + before do + command.apply_params!(params) + end + + context "when the service object is called" do + it "creates a new instance of Chef::Knife::KeyEdit with the correct args" do + expect(Chef::Knife::KeyEdit).to receive(:new). + with("charmander-key", "charmander", command.actor_field_name, command.ui, command.config). + and_return(service_object) + command.service_object + end + end # when the service object is called + end # after apply_params! is called with valid args + end # a key edit command + + describe Chef::Knife::UserKeyEdit do + it_should_behave_like "a key edit command" + # defined in key_helpers.rb + it_should_behave_like "a knife key command with a keyname as the second arg" + it_should_behave_like "a knife key command" do + let(:service_object) { instance_double(Chef::Knife::KeyEdit) } + let(:params) { ["charmander", "charmander-key"] } + end + end + + describe Chef::Knife::ClientKeyEdit do + it_should_behave_like "a key edit command" + # defined in key_helpers.rb + it_should_behave_like "a knife key command with a keyname as the second arg" + it_should_behave_like "a knife key command" do + let(:service_object) { instance_double(Chef::Knife::KeyEdit) } + let(:params) { ["charmander", "charmander-key"] } + end + end +end + +describe Chef::Knife::KeyEdit do + let(:public_key) { + "-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvPo+oNPB7uuNkws0fC02 +KxSwdyqPLu0fhI1pOweNKAZeEIiEz2PkybathHWy8snSXGNxsITkf3eyvIIKa8OZ +WrlqpI3yv/5DOP8HTMCxnFuMJQtDwMcevlqebX4bCxcByuBpNYDcAHjjfLGSfMjn +E5lZpgYWwnpic4kSjYcL9ORK9nYvlWV9P/kCYmRhIjB4AhtpWRiOfY/TKi3P2LxT +IjSmiN/ihHtlhV/VSnBJ5PzT/lRknlrJ4kACoz7Pq9jv+aAx5ft/xE9yDa2DYs0q +Tfuc9dUYsFjptWYrV6pfEQ+bgo1OGBXORBFcFL+2D7u9JYquKrMgosznHoEkQNLo +0wIDAQAB +-----END PUBLIC KEY-----" + } + let(:config) { Hash.new } + let(:actor) { "charmander" } + let(:keyname) { "charmander-key" } + let(:ui) { instance_double("Chef::Knife::UI") } + + shared_examples_for "key edit run command" do + let(:key_edit_object) { + described_class.new(keyname, actor, actor_field_name, ui, config) + } + + context "when the command is run" do + let(:expected_hash) { + { + actor_field_name => "charmander" + } + } + let(:new_keyname) { "charizard-key" } + + before do + allow(File).to receive(:read).and_return(public_key) + allow(File).to receive(:expand_path) + + allow(key_edit_object).to receive(:output_private_key_to_file) + allow(key_edit_object).to receive(:display_private_key) + allow(key_edit_object).to receive(:edit_data).and_return(expected_hash) + allow(key_edit_object).to receive(:display_info) + end + + + context "when public_key and create_key are passed" do + before do + key_edit_object.config[:public_key] = "public_key_path" + key_edit_object.config[:create_key] = true + end + + it "raises a Chef::Exceptions::KeyCommandInputError with the proper error message" do + expect{ key_edit_object.run }.to raise_error(Chef::Exceptions::KeyCommandInputError, key_edit_object.public_key_and_create_key_error_msg) + end + end + + context "when key_name is passed" do + let(:expected_hash) { + { + actor_field_name => "charmander", + "name" => new_keyname + } + } + before do + key_edit_object.config[:key_name] = new_keyname + allow_any_instance_of(Chef::Key).to receive(:update) + end + + it "update_key_from_hash gets passed a hash with new key name" do + expect(key_edit_object).to receive(:update_key_from_hash).with(expected_hash).and_return(Chef::Key.from_hash(expected_hash)) + key_edit_object.run + end + + it "Chef::Key.update is passed a string containing the original keyname" do + expect_any_instance_of(Chef::Key).to receive(:update).with(/#{keyname}/).and_return(Chef::Key.from_hash(expected_hash)) + key_edit_object.run + end + + it "Chef::Key.update is not passed a string containing the new keyname" do + expect_any_instance_of(Chef::Key).not_to receive(:update).with(/#{new_keyname}/) + allow_any_instance_of(Chef::Key).to receive(:update).and_return(Chef::Key.from_hash(expected_hash)) + key_edit_object.run + end + end + + context "when public_key, key_name, and expiration_date are passed" do + let(:expected_hash) { + { + actor_field_name => "charmander", + "public_key" => public_key, + "name" => new_keyname, + "expiration_date" => "infinity" + } + } + before do + key_edit_object.config[:public_key] = "this-public-key" + key_edit_object.config[:key_name] = new_keyname + key_edit_object.config[:expiration_date] = "infinity" + allow(key_edit_object).to receive(:update_key_from_hash).and_return(Chef::Key.from_hash(expected_hash)) + end + + it "passes the right hash to update_key_from_hash" do + expect(key_edit_object).to receive(:update_key_from_hash).with(expected_hash) + key_edit_object.run + end + end + + context "when create_key is passed" do + let(:expected_hash) { + { + actor_field_name => "charmander", + "create_key" => true + } + } + + before do + key_edit_object.config[:create_key] = true + allow(key_edit_object).to receive(:update_key_from_hash).and_return(Chef::Key.from_hash(expected_hash)) + end + + it "passes the right hash to update_key_from_hash" do + expect(key_edit_object).to receive(:update_key_from_hash).with(expected_hash) + key_edit_object.run + end + end + + context "when public_key is passed" do + let(:expected_hash) { + { + actor_field_name => "charmander", + "public_key" => public_key + } + } + before do + allow(key_edit_object).to receive(:update_key_from_hash).and_return(Chef::Key.from_hash(expected_hash)) + key_edit_object.config[:public_key] = "public_key_path" + end + + it "calls File.expand_path with the public_key input" do + expect(File).to receive(:expand_path).with("public_key_path") + key_edit_object.run + end + end # when public_key is passed + + context "when the server returns a private key" do + let(:expected_hash) { + { + actor_field_name => "charmander", + "public_key" => public_key, + "private_key" => "super_private" + } + } + + before do + allow(key_edit_object).to receive(:update_key_from_hash).and_return(Chef::Key.from_hash(expected_hash)) + key_edit_object.config[:public_key] = "public_key_path" + end + + context "when file is not passed" do + it "calls display_private_key with the private_key" do + expect(key_edit_object).to receive(:display_private_key).with("super_private") + key_edit_object.run + end + end + + context "when file is passed" do + before do + key_edit_object.config[:file] = "/fake/file" + end + + it "calls output_private_key_to_file with the private_key" do + expect(key_edit_object).to receive(:output_private_key_to_file).with("super_private") + key_edit_object.run + end + end + end # when the server returns a private key + + end # when the command is run + + + + end # key edit run command + + context "when actor_field_name is 'user'" do + it_should_behave_like "key edit run command" do + let(:actor_field_name) { "user" } + end + end + + context "when actor_field_name is 'client'" do + it_should_behave_like "key edit run command" do + let(:actor_field_name) { "client" } + end + end +end diff --git a/spec/unit/knife/key_helper.rb b/spec/unit/knife/key_helper.rb new file mode 100644 index 0000000000..36ababc09a --- /dev/null +++ b/spec/unit/knife/key_helper.rb @@ -0,0 +1,74 @@ +# +# Author:: Tyler Cloke (<tyler@chef.io>) +# 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' + +shared_examples_for "a knife key command" do + let(:stderr) { StringIO.new } + let(:params) { [] } + let(:command) do + c = described_class.new([]) + c.ui.config[:disable_editing] = true + allow(c.ui).to receive(:stderr).and_return(stderr) + allow(c.ui).to receive(:stdout).and_return(stderr) + allow(c).to receive(:show_usage) + c + end + + context "before apply_params! is called" do + context "when apply_params! is called with invalid args" do + it "shows the usage" do + expect(command).to receive(:show_usage) + expect { command.apply_params!(params) }.to exit_with_code(1) + end + + it "outputs the proper error" do + expect { command.apply_params!(params) }.to exit_with_code(1) + expect(stderr.string).to include(command.actor_missing_error) + end + + it "exits 1" do + expect { command.apply_params!(params) }.to exit_with_code(1) + end + end + end # before apply_params! is called + + context "after apply_params! is called with valid args" do + let(:params) { ["charmander"] } + before do + command.apply_params!(params) + end + + it "properly defines the actor" do + expect(command.actor).to eq("charmander") + end + end # after apply_params! is called with valid args + + context "when the command is run" do + before do + allow(command).to receive(:service_object).and_return(service_object) + allow(command).to receive(:name_args).and_return(["charmander"]) + end + + context "when the command is successful" do + before do + expect(service_object).to receive(:run) + end + end + end +end # a knife key command diff --git a/spec/unit/knife/key_list_spec.rb b/spec/unit/knife/key_list_spec.rb new file mode 100644 index 0000000000..aabe02ac02 --- /dev/null +++ b/spec/unit/knife/key_list_spec.rb @@ -0,0 +1,216 @@ +# +# Author:: Tyler Cloke (<tyler@chef.io>) +# 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/knife/user_key_list' +require 'chef/knife/client_key_list' +require 'chef/knife/key_list' +require 'chef/key' + +describe "key list commands that inherit knife" do + shared_examples_for "a key list command" do + let(:stderr) { StringIO.new } + let(:params) { [] } + let(:service_object) { instance_double(Chef::Knife::KeyList) } + let(:command) do + c = described_class.new([]) + c.ui.config[:disable_editing] = true + allow(c.ui).to receive(:stderr).and_return(stderr) + allow(c.ui).to receive(:stdout).and_return(stderr) + allow(c).to receive(:show_usage) + c + end + + context "after apply_params! is called with valid args" do + let(:params) { ["charmander"] } + before do + command.apply_params!(params) + end + + context "when the service object is called" do + it "creates a new instance of Chef::Knife::KeyList with the correct args" do + expect(Chef::Knife::KeyList).to receive(:new). + with("charmander", command.list_method, command.ui, command.config). + and_return(service_object) + command.service_object + end + end # when the service object is called + end # after apply_params! is called with valid args + end # a key list command + + describe Chef::Knife::UserKeyList do + it_should_behave_like "a key list command" + # defined in key_helpers.rb + it_should_behave_like "a knife key command" do + let(:service_object) { instance_double(Chef::Knife::KeyList) } + let(:params) { ["charmander"] } + end + end + + describe Chef::Knife::ClientKeyList do + it_should_behave_like "a key list command" + # defined in key_helpers.rb + it_should_behave_like "a knife key command" do + let(:service_object) { instance_double(Chef::Knife::KeyList) } + let(:params) { ["charmander"] } + end + end +end + +describe Chef::Knife::KeyList do + let(:config) { Hash.new } + let(:actor) { "charmander" } + let(:ui) { instance_double("Chef::Knife::UI") } + + shared_examples_for "key list run command" do + let(:key_list_object) { + described_class.new(actor, list_method, ui, config) + } + + before do + allow(Chef::Key).to receive(list_method).and_return(http_response) + allow(key_list_object).to receive(:display_info) + # simply pass the string though that colorize takes in + allow(key_list_object).to receive(:colorize).with(kind_of(String)) do |input| + input + end + end + + context "when only_expired and only_non_expired were both passed" do + before do + key_list_object.config[:only_expired] = true + key_list_object.config[:only_non_expired] = true + end + + it "raises a Chef::Exceptions::KeyCommandInputError with the proper error message" do + expect{ key_list_object.run }.to raise_error(Chef::Exceptions::KeyCommandInputError, key_list_object.expired_and_non_expired_msg) + end + end + + context "when the command is run" do + before do + key_list_object.config[:only_expired] = false + key_list_object.config[:only_non_expired] = false + key_list_object.config[:with_details] = false + end + + it "calls Chef::Key with the proper list command and input" do + expect(Chef::Key).to receive(list_method).with(actor) + key_list_object.run + end + + it "displays all the keys" do + expect(key_list_object).to receive(:display_info).with(/non-expired/).twice + expect(key_list_object).to receive(:display_info).with(/out-of-date/).once + key_list_object.run + end + + context "when only_expired is called" do + before do + key_list_object.config[:only_expired] = true + end + + it "excludes displaying non-expired keys" do + expect(key_list_object).to receive(:display_info).with(/non-expired/).exactly(0).times + key_list_object.run + end + + it "displays the expired keys" do + expect(key_list_object).to receive(:display_info).with(/out-of-date/).once + key_list_object.run + end + end # when only_expired is called + + context "when only_non_expired is called" do + before do + key_list_object.config[:only_non_expired] = true + end + + it "excludes displaying expired keys" do + expect(key_list_object).to receive(:display_info).with(/out-of-date/).exactly(0).times + key_list_object.run + end + + it "displays the non-expired keys" do + expect(key_list_object).to receive(:display_info).with(/non-expired/).twice + key_list_object.run + end + end # when only_expired is called + + context "when with_details is false" do + before do + key_list_object.config[:with_details] = false + end + + it "does not display the uri" do + expect(key_list_object).to receive(:display_info).with(/https/).exactly(0).times + key_list_object.run + end + + it "does not display the expired status" do + expect(key_list_object).to receive(:display_info).with(/\(expired\)/).exactly(0).times + key_list_object.run + end + end # when with_details is false + + context "when with_details is true" do + before do + key_list_object.config[:with_details] = true + end + + it "displays the uri" do + expect(key_list_object).to receive(:display_info).with(/https/).exactly(3).times + key_list_object.run + end + + it "displays the expired status" do + expect(key_list_object).to receive(:display_info).with(/\(expired\)/).once + key_list_object.run + end + end # when with_details is true + + end # when the command is run + + end # key list run command + + context "when list_method is :list_by_user" do + it_should_behave_like "key list run command" do + let(:list_method) { :list_by_user } + let(:http_response) { + [ + {"uri"=>"https://api.opscode.piab/users/charmander/keys/non-expired1", "name"=>"non-expired1", "expired"=>false}, + {"uri"=>"https://api.opscode.piab/users/charmander/keys/non-expired2", "name"=>"non-expired2", "expired"=>false}, + {"uri"=>"https://api.opscode.piab/users/mary/keys/out-of-date", "name"=>"out-of-date", "expired"=>true} + ] + } + end + end + + context "when list_method is :list_by_client" do + it_should_behave_like "key list run command" do + let(:list_method) { :list_by_client } + let(:http_response) { + [ + {"uri"=>"https://api.opscode.piab/organizations/pokemon/clients/charmander/keys/non-expired1", "name"=>"non-expired1", "expired"=>false}, + {"uri"=>"https://api.opscode.piab/organizations/pokemon/clients/charmander/keys/non-expired2", "name"=>"non-expired2", "expired"=>false}, + {"uri"=>"https://api.opscode.piab/organizations/pokemon/clients/mary/keys/out-of-date", "name"=>"out-of-date", "expired"=>true} + ] + } + end + end +end diff --git a/spec/unit/knife/key_show_spec.rb b/spec/unit/knife/key_show_spec.rb new file mode 100644 index 0000000000..5a0d839e4f --- /dev/null +++ b/spec/unit/knife/key_show_spec.rb @@ -0,0 +1,126 @@ +# +# Author:: Tyler Cloke (<tyler@chef.io>) +# 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/knife/user_key_show' +require 'chef/knife/client_key_show' +require 'chef/knife/key_show' +require 'chef/key' + +describe "key show commands that inherit knife" do + shared_examples_for "a key show command" do + let(:stderr) { StringIO.new } + let(:params) { [] } + let(:service_object) { instance_double(Chef::Knife::KeyShow) } + let(:command) do + c = described_class.new([]) + c.ui.config[:disable_editing] = true + allow(c.ui).to receive(:stderr).and_return(stderr) + allow(c.ui).to receive(:stdout).and_return(stderr) + allow(c).to receive(:show_usage) + c + end + + context "after apply_params! is called with valid args" do + let(:params) { ["charmander", "charmander-key"] } + before do + command.apply_params!(params) + end + + context "when the service object is called" do + it "creates a new instance of Chef::Knife::KeyShow with the correct args" do + expect(Chef::Knife::KeyShow).to receive(:new). + with("charmander-key", "charmander", command.load_method, command.ui). + and_return(service_object) + command.service_object + end + end # when the service object is called + end # after apply_params! is called with valid args + end # a key show command + + describe Chef::Knife::UserKeyShow do + it_should_behave_like "a key show command" + # defined in key_helpers.rb + it_should_behave_like "a knife key command with a keyname as the second arg" + it_should_behave_like "a knife key command" do + let(:service_object) { instance_double(Chef::Knife::KeyShow) } + let(:params) { ["charmander", "charmander-key"] } + end + end + + describe Chef::Knife::ClientKeyShow do + it_should_behave_like "a key show command" + # defined in key_helpers.rb + it_should_behave_like "a knife key command with a keyname as the second arg" + it_should_behave_like "a knife key command" do + let(:service_object) { instance_double(Chef::Knife::KeyShow) } + let(:params) { ["charmander", "charmander-key"] } + end + end +end + +describe Chef::Knife::KeyShow do + let(:actor) { "charmander" } + let(:keyname) { "charmander" } + let(:ui) { instance_double("Chef::Knife::UI") } + let(:expected_hash) { + { + actor_field_name => "charmander", + "name" => "charmander-key", + "public_key" => "some-public-key", + "expiration_date" => "infinity" + } + } + + shared_examples_for "key show run command" do + let(:key_show_object) { + described_class.new(keyname, actor, load_method, ui) + } + + before do + allow(key_show_object).to receive(:display_output) + allow(Chef::Key).to receive(load_method).and_return(Chef::Key.from_hash(expected_hash)) + end + + context "when the command is run" do + it "loads the key using the proper method and args" do + expect(Chef::Key).to receive(load_method).with(actor, keyname) + key_show_object.run + end + + it "displays the key" do + expect(key_show_object).to receive(:display_output) + key_show_object.run + end + end + end + + context "when load_method is :load_by_user" do + it_should_behave_like "key show run command" do + let(:load_method) { :load_by_user } + let(:actor_field_name) { 'user' } + end + end + + context "when load_method is :load_by_client" do + it_should_behave_like "key show run command" do + let(:load_method) { :load_by_client } + let(:actor_field_name) { 'user' } + end + end +end diff --git a/spec/unit/knife/osc_user_create_spec.rb b/spec/unit/knife/osc_user_create_spec.rb new file mode 100644 index 0000000000..e4ed78fe2b --- /dev/null +++ b/spec/unit/knife/osc_user_create_spec.rb @@ -0,0 +1,93 @@ +# +# Author:: Steven Danna (<steve@opscode.com>) +# Copyright:: Copyright (c) 2012 Opscode, 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' + +Chef::Knife::OscUserCreate.load_deps + +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur in user_create_spec.rb. + +describe Chef::Knife::OscUserCreate do + before(:each) do + @knife = Chef::Knife::OscUserCreate.new + + @stdout = StringIO.new + @stderr = StringIO.new + allow(@knife.ui).to receive(:stdout).and_return(@stdout) + allow(@knife.ui).to receive(:stderr).and_return(@stderr) + + @knife.name_args = [ 'a_user' ] + @knife.config[:user_password] = "foobar" + @user = Chef::User.new + @user.name "a_user" + @user_with_private_key = Chef::User.new + @user_with_private_key.name "a_user" + @user_with_private_key.private_key 'private_key' + allow(@user).to receive(:create).and_return(@user_with_private_key) + allow(Chef::User).to receive(:new).and_return(@user) + allow(Chef::User).to receive(:from_hash).and_return(@user) + allow(@knife).to receive(:edit_data).and_return(@user.to_hash) + end + + it "creates a new user" do + expect(Chef::User).to receive(:new).and_return(@user) + expect(@user).to receive(:create) + @knife.run + expect(@stderr.string).to match /created user.+a_user/i + end + + it "sets the password" do + @knife.config[:user_password] = "a_password" + expect(@user).to receive(:password).with("a_password") + @knife.run + end + + it "exits with an error if password is blank" do + @knife.config[:user_password] = '' + expect { @knife.run }.to raise_error SystemExit + expect(@stderr.string).to match /You must specify a non-blank password/ + end + + it "sets the user name" do + expect(@user).to receive(:name).with("a_user") + @knife.run + end + + it "sets the public key if given" do + @knife.config[:user_key] = "/a/filename" + allow(File).to receive(:read).with(File.expand_path("/a/filename")).and_return("a_key") + expect(@user).to receive(:public_key).with("a_key") + @knife.run + end + + it "allows you to edit the data" do + expect(@knife).to receive(:edit_data).with(@user) + @knife.run + end + + it "writes the private key to a file when --file is specified" do + @knife.config[:file] = "/tmp/a_file" + filehandle = double("filehandle") + expect(filehandle).to receive(:print).with('private_key') + expect(File).to receive(:open).with("/tmp/a_file", "w").and_yield(filehandle) + @knife.run + end +end diff --git a/spec/unit/knife/osc_user_delete_spec.rb b/spec/unit/knife/osc_user_delete_spec.rb new file mode 100644 index 0000000000..4a3ec4228f --- /dev/null +++ b/spec/unit/knife/osc_user_delete_spec.rb @@ -0,0 +1,44 @@ +# +# Author:: Steven Danna (<steve@opscode.com>) +# Copyright:: Copyright (c) 2012 Opscode, 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' + +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur in user_delete_spec.rb. + +describe Chef::Knife::OscUserDelete do + before(:each) do + Chef::Knife::OscUserDelete.load_deps + @knife = Chef::Knife::OscUserDelete.new + @knife.name_args = [ 'my_user' ] + end + + it 'deletes the user' do + expect(@knife).to receive(:delete_object).with(Chef::User, 'my_user') + @knife.run + end + + it 'prints usage and exits when a user name is not provided' do + @knife.name_args = [] + expect(@knife).to receive(:show_usage) + expect(@knife.ui).to receive(:fatal) + expect { @knife.run }.to raise_error(SystemExit) + end +end diff --git a/spec/unit/knife/osc_user_edit_spec.rb b/spec/unit/knife/osc_user_edit_spec.rb new file mode 100644 index 0000000000..279f2e30ef --- /dev/null +++ b/spec/unit/knife/osc_user_edit_spec.rb @@ -0,0 +1,52 @@ +# +# Author:: Steven Danna (<steve@opscode.com>) +# Copyright:: Copyright (c) 2012 Opscode, 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' + +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur in user_edit_spec.rb. + +describe Chef::Knife::OscUserEdit do + before(:each) do + @stderr = StringIO.new + @stdout = StringIO.new + + Chef::Knife::OscUserEdit.load_deps + @knife = Chef::Knife::OscUserEdit.new + allow(@knife.ui).to receive(:stderr).and_return(@stderr) + allow(@knife.ui).to receive(:stdout).and_return(@stdout) + @knife.name_args = [ 'my_user' ] + @knife.config[:disable_editing] = true + end + + it 'loads and edits the user' do + data = { :name => "my_user" } + allow(Chef::User).to receive(:load).with("my_user").and_return(data) + expect(@knife).to receive(:edit_data).with(data).and_return(data) + @knife.run + end + + it 'prints usage and exits when a user name is not provided' do + @knife.name_args = [] + expect(@knife).to receive(:show_usage) + expect(@knife.ui).to receive(:fatal) + expect { @knife.run }.to raise_error(SystemExit) + end +end diff --git a/spec/unit/provider/powershell_spec.rb b/spec/unit/knife/osc_user_list_spec.rb index 60dbcf80b0..f496a414b8 100644 --- a/spec/unit/provider/powershell_spec.rb +++ b/spec/unit/knife/osc_user_list_spec.rb @@ -1,6 +1,6 @@ # -# Author:: Adam Edwards (<adamed@opscode.com>) -# Copyright:: Copyright (c) 2013 Opscode, Inc. +# Author:: Steven Danna +# Copyright:: Copyright (c) 2012 Opscode, Inc # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,22 +17,21 @@ # require 'spec_helper' -describe Chef::Provider::PowershellScript, "action_run" do - before(:each) do - @node = Chef::Node.new - - @node.default["kernel"] = Hash.new - @node.default["kernel"][:machine] = :x86_64.to_s - - @run_context = Chef::RunContext.new(@node, {}, @events) - @new_resource = Chef::Resource::PowershellScript.new('run some powershell code', @run_context) +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur in user_list_spec.rb. - @provider = Chef::Provider::PowershellScript.new(@new_resource, @run_context) +describe Chef::Knife::OscUserList do + before(:each) do + Chef::Knife::OscUserList.load_deps + @knife = Chef::Knife::OscUserList.new end - it "should set the -File flag as the last flag" do - expect(@provider.flags.split(' ').pop).to eq("-File") + it 'lists the users' do + expect(Chef::User).to receive(:list) + expect(@knife).to receive(:format_list_for_display) + @knife.run end - end diff --git a/spec/unit/knife/osc_user_reregister_spec.rb b/spec/unit/knife/osc_user_reregister_spec.rb new file mode 100644 index 0000000000..989eb180f1 --- /dev/null +++ b/spec/unit/knife/osc_user_reregister_spec.rb @@ -0,0 +1,58 @@ +# +# Author:: Steven Danna (<steve@opscode.com>) +# Copyright:: Copyright (c) 2012 Opscode, 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' + +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur in user_reregister_spec.rb. + +describe Chef::Knife::OscUserReregister do + before(:each) do + Chef::Knife::OscUserReregister.load_deps + @knife = Chef::Knife::OscUserReregister.new + @knife.name_args = [ 'a_user' ] + @user_mock = double('user_mock', :private_key => "private_key") + allow(Chef::User).to receive(:load).and_return(@user_mock) + @stdout = StringIO.new + allow(@knife.ui).to receive(:stdout).and_return(@stdout) + end + + it 'prints usage and exits when a user name is not provided' do + @knife.name_args = [] + expect(@knife).to receive(:show_usage) + expect(@knife.ui).to receive(:fatal) + expect { @knife.run }.to raise_error(SystemExit) + end + + it 'reregisters the user and prints the key' do + expect(@user_mock).to receive(:reregister).and_return(@user_mock) + @knife.run + expect(@stdout.string).to match( /private_key/ ) + end + + it 'writes the private key to a file when --file is specified' do + expect(@user_mock).to receive(:reregister).and_return(@user_mock) + @knife.config[:file] = '/tmp/a_file' + filehandle = StringIO.new + expect(File).to receive(:open).with('/tmp/a_file', 'w').and_yield(filehandle) + @knife.run + expect(filehandle.string).to eq("private_key") + end +end diff --git a/spec/unit/knife/osc_user_show_spec.rb b/spec/unit/knife/osc_user_show_spec.rb new file mode 100644 index 0000000000..18d2086099 --- /dev/null +++ b/spec/unit/knife/osc_user_show_spec.rb @@ -0,0 +1,46 @@ +# +# Author:: Steven Danna (<steve@opscode.com>) +# Copyright:: Copyright (c) 2012 Opscode, 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' + +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur user_show_spec.rb. + +describe Chef::Knife::OscUserShow do + before(:each) do + Chef::Knife::OscUserShow.load_deps + @knife = Chef::Knife::OscUserShow.new + @knife.name_args = [ 'my_user' ] + @user_mock = double('user_mock') + end + + it 'loads and displays the user' do + expect(Chef::User).to receive(:load).with('my_user').and_return(@user_mock) + expect(@knife).to receive(:format_for_display).with(@user_mock) + @knife.run + end + + it 'prints usage and exits when a user name is not provided' do + @knife.name_args = [] + expect(@knife).to receive(:show_usage) + expect(@knife.ui).to receive(:fatal) + expect { @knife.run }.to raise_error(SystemExit) + end +end diff --git a/spec/unit/knife/ssh_spec.rb b/spec/unit/knife/ssh_spec.rb index 501b02c933..723280bead 100644 --- a/spec/unit/knife/ssh_spec.rb +++ b/spec/unit/knife/ssh_spec.rb @@ -28,10 +28,10 @@ describe Chef::Knife::Ssh do before do @knife = Chef::Knife::Ssh.new @knife.merge_configs - @knife.config[:attribute] = "fqdn" @node_foo = Chef::Node.new @node_foo.automatic_attrs[:fqdn] = "foo.example.org" @node_foo.automatic_attrs[:ipaddress] = "10.0.0.1" + @node_bar = Chef::Node.new @node_bar.automatic_attrs[:fqdn] = "bar.example.org" @node_bar.automatic_attrs[:ipaddress] = "10.0.0.2" @@ -52,15 +52,15 @@ describe Chef::Knife::Ssh do def self.should_return_specified_attributes it "returns an array of the attributes specified on the command line OR config file, if only one is set" do @knife.config[:attribute] = "ipaddress" - @knife.config[:attribute_from_cli] = "ipaddress" + Chef::Config[:knife][:ssh_attribute] = "ipaddress" # this value will be in the config file configure_query([@node_foo, @node_bar]) expect(@knife).to receive(:session_from_list).with([['10.0.0.1', nil], ['10.0.0.2', nil]]) @knife.configure_session end it "returns an array of the attributes specified on the command line even when a config value is set" do - @knife.config[:attribute] = "config_file" # this value will be the config file - @knife.config[:attribute_from_cli] = "ipaddress" # this is the value of the command line via #configure_attribute + Chef::Config[:knife][:ssh_attribute] = "config_file" # this value will be in the config file + @knife.config[:attribute] = "ipaddress" # this is the value of the command line via #configure_attribute configure_query([@node_foo, @node_bar]) expect(@knife).to receive(:session_from_list).with([['10.0.0.1', nil], ['10.0.0.2', nil]]) @knife.configure_session @@ -83,7 +83,6 @@ describe Chef::Knife::Ssh do @node_foo.automatic_attrs[:cloud][:public_hostname] = "ec2-10-0-0-1.compute-1.amazonaws.com" @node_bar.automatic_attrs[:cloud][:public_hostname] = "ec2-10-0-0-2.compute-1.amazonaws.com" end - it "returns an array of cloud public hostnames" do configure_query([@node_foo, @node_bar]) expect(@knife).to receive(:session_from_list).with([ @@ -96,6 +95,24 @@ describe Chef::Knife::Ssh do should_return_specified_attributes end + context "when cloud hostnames are available but empty" do + before do + @node_foo.automatic_attrs[:cloud][:public_hostname] = '' + @node_bar.automatic_attrs[:cloud][:public_hostname] = '' + end + + it "returns an array of fqdns" do + configure_query([@node_foo, @node_bar]) + expect(@knife).to receive(:session_from_list).with([ + ['foo.example.org', nil], + ['bar.example.org', nil] + ]) + @knife.configure_session + end + + should_return_specified_attributes + end + it "should raise an error if no host are found" do configure_query([ ]) expect(@knife.ui).to receive(:fatal) @@ -132,42 +149,40 @@ describe Chef::Knife::Ssh do end end - describe "#configure_attribute" do + describe "#get_ssh_attribute" do + # Order of precedence for ssh target + # 1) command line attribute + # 2) configuration file + # 3) cloud attribute + # 4) fqdn before do Chef::Config[:knife][:ssh_attribute] = nil @knife.config[:attribute] = nil + @node_foo.automatic_attrs[:cloud][:public_hostname] = "ec2-10-0-0-1.compute-1.amazonaws.com" + @node_bar.automatic_attrs[:cloud][:public_hostname] = '' end it "should return fqdn by default" do - @knife.configure_attribute - expect(@knife.config[:attribute]).to eq("fqdn") + expect(@knife.get_ssh_attribute(Chef::Node.new)).to eq("fqdn") end - it "should return the value set in the configuration file" do - Chef::Config[:knife][:ssh_attribute] = "config_file" - @knife.configure_attribute - expect(@knife.config[:attribute]).to eq("config_file") + it "should return cloud.public_hostname attribute if available" do + expect(@knife.get_ssh_attribute(@node_foo)).to eq("cloud.public_hostname") end - it "should return the value set on the command line" do + it "should favor to attribute_from_cli over config file and cloud" do @knife.config[:attribute] = "command_line" - @knife.configure_attribute - expect(@knife.config[:attribute]).to eq("command_line") + Chef::Config[:knife][:ssh_attribute] = "config_file" + expect( @knife.get_ssh_attribute(@node_foo)).to eq("command_line") end - it "should set attribute_from_cli to the value of attribute from the command line" do - @knife.config[:attribute] = "command_line" - @knife.configure_attribute - expect(@knife.config[:attribute]).to eq("command_line") - expect(@knife.config[:attribute_from_cli]).to eq("command_line") + it "should favor config file over cloud and default" do + Chef::Config[:knife][:ssh_attribute] = "config_file" + expect( @knife.get_ssh_attribute(@node_foo)).to eq("config_file") end - it "should prefer the command line over the config file for the value of attribute_from_cli" do - Chef::Config[:knife][:ssh_attribute] = "config_file" - @knife.config[:attribute] = "command_line" - @knife.configure_attribute - expect(@knife.config[:attribute]).to eq("command_line") - expect(@knife.config[:attribute_from_cli]).to eq("command_line") + it "should return fqdn if cloud.hostname is empty" do + expect( @knife.get_ssh_attribute(@node_bar)).to eq("fqdn") end end diff --git a/spec/unit/knife/ssl_check_spec.rb b/spec/unit/knife/ssl_check_spec.rb index 8eda555108..fd46c47d99 100644 --- a/spec/unit/knife/ssl_check_spec.rb +++ b/spec/unit/knife/ssl_check_spec.rb @@ -163,6 +163,7 @@ E expect(ssl_check).to receive(:verify_X509).and_return(true) # X509 valid certs (no warn) expect(ssl_socket).to receive(:connect) # no error expect(ssl_socket).to receive(:post_connection_check).with("foo.example.com") # no error + expect(ssl_socket).to receive(:hostname=).with("foo.example.com") # no error end it "prints a success message" do @@ -197,6 +198,7 @@ E expect(ssl_socket).to receive(:post_connection_check). with("foo.example.com"). and_raise(OpenSSL::SSL::SSLError) + expect(ssl_socket).to receive(:hostname=).with("foo.example.com") # no error expect(ssl_socket_for_debug).to receive(:connect) expect(ssl_socket_for_debug).to receive(:peer_cert).and_return(self_signed_crt) end @@ -215,6 +217,8 @@ E expect(ssl_check).to receive(:verify_X509).and_return(true) # X509 valid certs expect(ssl_socket).to receive(:connect). and_raise(OpenSSL::SSL::SSLError) + expect(ssl_socket).to receive(:hostname=). + with("foo.example.com") # no error expect(ssl_socket_for_debug).to receive(:connect) expect(ssl_socket_for_debug).to receive(:peer_cert).and_return(self_signed_crt) end diff --git a/spec/unit/knife/status_spec.rb b/spec/unit/knife/status_spec.rb index 2522bc61b1..ee44f3b3fd 100644 --- a/spec/unit/knife/status_spec.rb +++ b/spec/unit/knife/status_spec.rb @@ -24,15 +24,81 @@ describe Chef::Knife::Status do n.automatic_attrs["fqdn"] = "foobar" n.automatic_attrs["ohai_time"] = 1343845969 end - query = double("Chef::Search::Query") - allow(query).to receive(:search).and_yield(node) - allow(Chef::Search::Query).to receive(:new).and_return(query) + allow(Time).to receive(:now).and_return(Time.at(1428573420)) + @query = double("Chef::Search::Query") + allow(@query).to receive(:search).and_yield(node) + allow(Chef::Search::Query).to receive(:new).and_return(@query) @knife = Chef::Knife::Status.new @stdout = StringIO.new allow(@knife.ui).to receive(:stdout).and_return(@stdout) end describe "run" do + let(:opts) {{filter_result: + { name: ["name"], ipaddress: ["ipaddress"], ohai_time: ["ohai_time"], + ec2: ["ec2"], run_list: ["run_list"], platform: ["platform"], + platform_version: ["platform_version"], chef_environment: ["chef_environment"]}}} + + it "should default to searching for everything" do + expect(@query).to receive(:search).with(:node, "*:*", opts) + @knife.run + end + + it "should filter healthy nodes" do + @knife.config[:hide_healthy] = true + expect(@query).to receive(:search).with(:node, "NOT ohai_time:[1428569820 TO 1428573420]", opts) + @knife.run + end + + it "should filter by environment" do + @knife.config[:environment] = "production" + expect(@query).to receive(:search).with(:node, "chef_environment:production", opts) + @knife.run + end + + it "should filter by environment and health" do + @knife.config[:environment] = "production" + @knife.config[:hide_healthy] = true + expect(@query).to receive(:search).with(:node, "chef_environment:production NOT ohai_time:[1428569820 TO 1428573420]", opts) + @knife.run + end + + it "should not use partial search with long output" do + @knife.config[:long_output] = true + expect(@query).to receive(:search).with(:node, "*:*", {}) + @knife.run + end + + context "with a custom query" do + before :each do + @knife.instance_variable_set(:@name_args, ["name:my_custom_name"]) + end + + it "should allow a custom query to be specified" do + expect(@query).to receive(:search).with(:node, "name:my_custom_name", opts) + @knife.run + end + + it "should filter healthy nodes" do + @knife.config[:hide_healthy] = true + expect(@query).to receive(:search).with(:node, "name:my_custom_name NOT ohai_time:[1428569820 TO 1428573420]", opts) + @knife.run + end + + it "should filter by environment" do + @knife.config[:environment] = "production" + expect(@query).to receive(:search).with(:node, "name:my_custom_name AND chef_environment:production", opts) + @knife.run + end + + it "should filter by environment and health" do + @knife.config[:environment] = "production" + @knife.config[:hide_healthy] = true + expect(@query).to receive(:search).with(:node, "name:my_custom_name AND chef_environment:production NOT ohai_time:[1428569820 TO 1428573420]", opts) + @knife.run + end + end + it "should not colorize output unless it's writing to a tty" do @knife.run expect(@stdout.string.match(/foobar/)).not_to be_nil diff --git a/spec/unit/knife/user_create_spec.rb b/spec/unit/knife/user_create_spec.rb index ad8821cd0e..fa5c8324b4 100644 --- a/spec/unit/knife/user_create_spec.rb +++ b/spec/unit/knife/user_create_spec.rb @@ -1,6 +1,7 @@ # -# Author:: Steven Danna (<steve@opscode.com>) -# Copyright:: Copyright (c) 2012 Opscode, Inc. +# Author:: Steven Danna (<steve@chef.io>) +# Author:: Tyler Cloke (<tyler@chef.io>) +# Copyright:: Copyright (c) 2012, 2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,68 +22,193 @@ require 'spec_helper' Chef::Knife::UserCreate.load_deps describe Chef::Knife::UserCreate do + let(:knife) { Chef::Knife::UserCreate.new } + + let(:stderr) { + StringIO.new + } + + let(:stdout) { + StringIO.new + } + before(:each) do - @knife = Chef::Knife::UserCreate.new - - @stdout = StringIO.new - @stderr = StringIO.new - allow(@knife.ui).to receive(:stdout).and_return(@stdout) - allow(@knife.ui).to receive(:stderr).and_return(@stderr) - - @knife.name_args = [ 'a_user' ] - @knife.config[:user_password] = "foobar" - @user = Chef::User.new - @user.name "a_user" - @user_with_private_key = Chef::User.new - @user_with_private_key.name "a_user" - @user_with_private_key.private_key 'private_key' - allow(@user).to receive(:create).and_return(@user_with_private_key) - allow(Chef::User).to receive(:new).and_return(@user) - allow(Chef::User).to receive(:from_hash).and_return(@user) - allow(@knife).to receive(:edit_data).and_return(@user.to_hash) + allow(knife.ui).to receive(:stdout).and_return(stdout) + allow(knife.ui).to receive(:stderr).and_return(stderr) + allow(knife.ui).to receive(:warn) end - it "creates a new user" do - expect(Chef::User).to receive(:new).and_return(@user) - expect(@user).to receive(:create) - @knife.run - expect(@stderr.string).to match /created user.+a_user/i - end + # delete this once OSC11 support is gone + context "when only one name_arg is passed" do + before do + knife.name_args = ['some_user'] + allow(knife).to receive(:run_osc_11_user_create).and_raise(SystemExit) + end + + it "displays the osc warning" do + expect(knife.ui).to receive(:warn).with(knife.osc_11_warning) + expect{ knife.run }.to raise_error(SystemExit) + end + + it "calls knife osc_user create" do + expect(knife).to receive(:run_osc_11_user_create) + expect{ knife.run }.to raise_error(SystemExit) + end - it "sets the password" do - @knife.config[:user_password] = "a_password" - expect(@user).to receive(:password).with("a_password") - @knife.run end - it "exits with an error if password is blank" do - @knife.config[:user_password] = '' - expect { @knife.run }.to raise_error SystemExit - expect(@stderr.string).to match /You must specify a non-blank password/ + context "when USERNAME isn't specified" do + # from spec/support/shared/unit/knife_shared.rb + it_should_behave_like "mandatory field missing" do + let(:name_args) { [] } + let(:fieldname) { 'username' } + end end - it "sets the user name" do - expect(@user).to receive(:name).with("a_user") - @knife.run + # uncomment once OSC11 support is gone, + # pending doesn't work for shared_examples_for by default + # + # context "when DISPLAY_NAME isn't specified" do + # # from spec/support/shared/unit/knife_shared.rb + # it_should_behave_like "mandatory field missing" do + # let(:name_args) { ['some_user'] } + # let(:fieldname) { 'display name' } + # end + # end + + context "when FIRST_NAME isn't specified" do + # from spec/support/shared/unit/knife_shared.rb + it_should_behave_like "mandatory field missing" do + let(:name_args) { ['some_user', 'some_display_name'] } + let(:fieldname) { 'first name' } + end end - it "sets the public key if given" do - @knife.config[:user_key] = "/a/filename" - allow(File).to receive(:read).with(File.expand_path("/a/filename")).and_return("a_key") - expect(@user).to receive(:public_key).with("a_key") - @knife.run + context "when LAST_NAME isn't specified" do + # from spec/support/shared/unit/knife_shared.rb + it_should_behave_like "mandatory field missing" do + let(:name_args) { ['some_user', 'some_display_name', 'some_first_name'] } + let(:fieldname) { 'last name' } + end end - it "allows you to edit the data" do - expect(@knife).to receive(:edit_data).with(@user) - @knife.run + context "when EMAIL isn't specified" do + # from spec/support/shared/unit/knife_shared.rb + it_should_behave_like "mandatory field missing" do + let(:name_args) { ['some_user', 'some_display_name', 'some_first_name', 'some_last_name'] } + let(:fieldname) { 'email' } + end end - it "writes the private key to a file when --file is specified" do - @knife.config[:file] = "/tmp/a_file" - filehandle = double("filehandle") - expect(filehandle).to receive(:print).with('private_key') - expect(File).to receive(:open).with("/tmp/a_file", "w").and_yield(filehandle) - @knife.run + context "when PASSWORD isn't specified" do + # from spec/support/shared/unit/knife_shared.rb + it_should_behave_like "mandatory field missing" do + let(:name_args) { ['some_user', 'some_display_name', 'some_first_name', 'some_last_name', 'some_email'] } + let(:fieldname) { 'password' } + end end + + context "when all mandatory fields are validly specified" do + before do + knife.name_args = ['some_user', 'some_display_name', 'some_first_name', 'some_last_name', 'some_email', 'some_password'] + allow(knife).to receive(:edit_data).and_return(knife.user.to_hash) + allow(knife).to receive(:create_user_from_hash).and_return(knife.user) + end + + before(:each) do + # reset the user field every run + knife.user_field = nil + end + + it "sets all the mandatory fields" do + knife.run + expect(knife.user.username).to eq('some_user') + expect(knife.user.display_name).to eq('some_display_name') + expect(knife.user.first_name).to eq('some_first_name') + expect(knife.user.last_name).to eq('some_last_name') + expect(knife.user.email).to eq('some_email') + expect(knife.user.password).to eq('some_password') + end + + context "when user_key and prevent_keygen are passed" do + before do + knife.config[:user_key] = "some_key" + knife.config[:prevent_keygen] = true + end + it "prints the usage" do + expect(knife).to receive(:show_usage) + expect { knife.run }.to raise_error(SystemExit) + end + + it "prints a relevant error message" do + expect { knife.run }.to raise_error(SystemExit) + expect(stderr.string).to match /You cannot pass --user-key and --prevent-keygen/ + end + end + + context "when --prevent-keygen is passed" do + before do + knife.config[:prevent_keygen] = true + end + + it "does not set user.create_key" do + knife.run + expect(knife.user.create_key).to be_falsey + end + end + + context "when --prevent-keygen is not passed" do + it "sets user.create_key to true" do + knife.run + expect(knife.user.create_key).to be_truthy + end + end + + context "when --user-key is passed" do + before do + knife.config[:user_key] = 'some_key' + allow(File).to receive(:read).and_return('some_key') + allow(File).to receive(:expand_path) + end + + it "sets user.public_key" do + knife.run + expect(knife.user.public_key).to eq('some_key') + end + end + + context "when --user-key is not passed" do + it "does not set user.public_key" do + knife.run + expect(knife.user.public_key).to be_nil + end + end + + context "when a private_key is returned" do + before do + allow(knife).to receive(:create_user_from_hash).and_return(Chef::UserV1.from_hash(knife.user.to_hash.merge({"private_key" => "some_private_key"}))) + end + + context "when --file is passed" do + before do + knife.config[:file] = '/some/path' + end + + it "creates a new file of the path passed" do + filehandle = double('filehandle') + expect(filehandle).to receive(:print).with('some_private_key') + expect(File).to receive(:open).with('/some/path', 'w').and_yield(filehandle) + knife.run + end + end + + context "when --file is not passed" do + it "prints the private key to stdout" do + expect(knife.ui).to receive(:msg).with('some_private_key') + knife.run + end + end + end + + end # when all mandatory fields are validly specified end diff --git a/spec/unit/knife/user_delete_spec.rb b/spec/unit/knife/user_delete_spec.rb index 94cfbf3db1..a24160624a 100644 --- a/spec/unit/knife/user_delete_spec.rb +++ b/spec/unit/knife/user_delete_spec.rb @@ -19,21 +19,47 @@ require 'spec_helper' describe Chef::Knife::UserDelete do + let(:knife) { Chef::Knife::UserDelete.new } + let(:user) { double('user_object') } + let(:stdout) { StringIO.new } + before(:each) do Chef::Knife::UserDelete.load_deps - @knife = Chef::Knife::UserDelete.new - @knife.name_args = [ 'my_user' ] + knife.name_args = [ 'my_user' ] + allow(Chef::UserV1).to receive(:load).and_return(user) + allow(user).to receive(:username).and_return('my_user') + allow(knife.ui).to receive(:stderr).and_return(stdout) + allow(knife.ui).to receive(:stdout).and_return(stdout) + end + + # delete this once OSC11 support is gone + context "when the username field is not supported by the server" do + before do + allow(knife).to receive(:run_osc_11_user_delete).and_raise(SystemExit) + allow(user).to receive(:username).and_return(nil) + end + + it "displays the osc warning" do + expect(knife.ui).to receive(:warn).with(knife.osc_11_warning) + expect{ knife.run }.to raise_error(SystemExit) + end + + it "forwards the command to knife osc_user edit" do + expect(knife).to receive(:run_osc_11_user_delete) + expect{ knife.run }.to raise_error(SystemExit) + end end it 'deletes the user' do - expect(@knife).to receive(:delete_object).with(Chef::User, 'my_user') - @knife.run + #expect(knife).to receive(:delete_object).with(Chef::UserV1, 'my_user') + expect(knife).to receive(:delete_object).with('my_user') + knife.run end it 'prints usage and exits when a user name is not provided' do - @knife.name_args = [] - expect(@knife).to receive(:show_usage) - expect(@knife.ui).to receive(:fatal) - expect { @knife.run }.to raise_error(SystemExit) + knife.name_args = [] + expect(knife).to receive(:show_usage) + expect(knife.ui).to receive(:fatal) + expect { knife.run }.to raise_error(SystemExit) end end diff --git a/spec/unit/knife/user_edit_spec.rb b/spec/unit/knife/user_edit_spec.rb index 0eb75cfa9b..a21d982d29 100644 --- a/spec/unit/knife/user_edit_spec.rb +++ b/spec/unit/knife/user_edit_spec.rb @@ -19,29 +19,48 @@ require 'spec_helper' describe Chef::Knife::UserEdit do + let(:knife) { Chef::Knife::UserEdit.new } + before(:each) do @stderr = StringIO.new @stdout = StringIO.new Chef::Knife::UserEdit.load_deps - @knife = Chef::Knife::UserEdit.new - allow(@knife.ui).to receive(:stderr).and_return(@stderr) - allow(@knife.ui).to receive(:stdout).and_return(@stdout) - @knife.name_args = [ 'my_user' ] - @knife.config[:disable_editing] = true + allow(knife.ui).to receive(:stderr).and_return(@stderr) + allow(knife.ui).to receive(:stdout).and_return(@stdout) + knife.name_args = [ 'my_user' ] + knife.config[:disable_editing] = true + end + + # delete this once OSC11 support is gone + context "when the username field is not supported by the server" do + before do + allow(knife).to receive(:run_osc_11_user_edit).and_raise(SystemExit) + allow(Chef::UserV1).to receive(:load).and_return({"username" => nil}) + end + + it "displays the osc warning" do + expect(knife.ui).to receive(:warn).with(knife.osc_11_warning) + expect{ knife.run }.to raise_error(SystemExit) + end + + it "forwards the command to knife osc_user edit" do + expect(knife).to receive(:run_osc_11_user_edit) + expect{ knife.run }.to raise_error(SystemExit) + end end it 'loads and edits the user' do - data = { :name => "my_user" } - allow(Chef::User).to receive(:load).with("my_user").and_return(data) - expect(@knife).to receive(:edit_data).with(data).and_return(data) - @knife.run + data = { "username" => "my_user" } + allow(Chef::UserV1).to receive(:load).with("my_user").and_return(data) + expect(knife).to receive(:edit_data).with(data).and_return(data) + knife.run end it 'prints usage and exits when a user name is not provided' do - @knife.name_args = [] - expect(@knife).to receive(:show_usage) - expect(@knife.ui).to receive(:fatal) - expect { @knife.run }.to raise_error(SystemExit) + knife.name_args = [] + expect(knife).to receive(:show_usage) + expect(knife.ui).to receive(:fatal) + expect { knife.run }.to raise_error(SystemExit) end end diff --git a/spec/unit/knife/user_list_spec.rb b/spec/unit/knife/user_list_spec.rb index db097a5c16..fa2bac426e 100644 --- a/spec/unit/knife/user_list_spec.rb +++ b/spec/unit/knife/user_list_spec.rb @@ -19,14 +19,18 @@ require 'spec_helper' describe Chef::Knife::UserList do + let(:knife) { Chef::Knife::UserList.new } + let(:stdout) { StringIO.new } + before(:each) do Chef::Knife::UserList.load_deps - @knife = Chef::Knife::UserList.new + allow(knife.ui).to receive(:stderr).and_return(stdout) + allow(knife.ui).to receive(:stdout).and_return(stdout) end it 'lists the users' do - expect(Chef::User).to receive(:list) - expect(@knife).to receive(:format_list_for_display) - @knife.run + expect(Chef::UserV1).to receive(:list) + expect(knife).to receive(:format_list_for_display) + knife.run end end diff --git a/spec/unit/knife/user_reregister_spec.rb b/spec/unit/knife/user_reregister_spec.rb index 1268716f40..89aa6726cd 100644 --- a/spec/unit/knife/user_reregister_spec.rb +++ b/spec/unit/knife/user_reregister_spec.rb @@ -19,35 +19,56 @@ require 'spec_helper' describe Chef::Knife::UserReregister do - before(:each) do + let(:knife) { Chef::Knife::UserReregister.new } + let(:user_mock) { double('user_mock', :private_key => "private_key") } + let(:stdout) { StringIO.new } + + before do Chef::Knife::UserReregister.load_deps - @knife = Chef::Knife::UserReregister.new - @knife.name_args = [ 'a_user' ] - @user_mock = double('user_mock', :private_key => "private_key") - allow(Chef::User).to receive(:load).and_return(@user_mock) - @stdout = StringIO.new - allow(@knife.ui).to receive(:stdout).and_return(@stdout) + knife.name_args = [ 'a_user' ] + allow(Chef::UserV1).to receive(:load).and_return(user_mock) + allow(knife.ui).to receive(:stdout).and_return(stdout) + allow(knife.ui).to receive(:stderr).and_return(stdout) + allow(user_mock).to receive(:username).and_return('a_user') + end + + # delete this once OSC11 support is gone + context "when the username field is not supported by the server" do + before do + allow(knife).to receive(:run_osc_11_user_reregister).and_raise(SystemExit) + allow(user_mock).to receive(:username).and_return(nil) + end + + it "displays the osc warning" do + expect(knife.ui).to receive(:warn).with(knife.osc_11_warning) + expect{ knife.run }.to raise_error(SystemExit) + end + + it "forwards the command to knife osc_user edit" do + expect(knife).to receive(:run_osc_11_user_reregister) + expect{ knife.run }.to raise_error(SystemExit) + end end it 'prints usage and exits when a user name is not provided' do - @knife.name_args = [] - expect(@knife).to receive(:show_usage) - expect(@knife.ui).to receive(:fatal) - expect { @knife.run }.to raise_error(SystemExit) + knife.name_args = [] + expect(knife).to receive(:show_usage) + expect(knife.ui).to receive(:fatal) + expect { knife.run }.to raise_error(SystemExit) end it 'reregisters the user and prints the key' do - expect(@user_mock).to receive(:reregister).and_return(@user_mock) - @knife.run - expect(@stdout.string).to match( /private_key/ ) + expect(user_mock).to receive(:reregister).and_return(user_mock) + knife.run + expect(stdout.string).to match( /private_key/ ) end it 'writes the private key to a file when --file is specified' do - expect(@user_mock).to receive(:reregister).and_return(@user_mock) - @knife.config[:file] = '/tmp/a_file' + expect(user_mock).to receive(:reregister).and_return(user_mock) + knife.config[:file] = '/tmp/a_file' filehandle = StringIO.new expect(File).to receive(:open).with('/tmp/a_file', 'w').and_yield(filehandle) - @knife.run + knife.run expect(filehandle.string).to eq("private_key") end end diff --git a/spec/unit/knife/user_show_spec.rb b/spec/unit/knife/user_show_spec.rb index f97cbc3f13..7c39e428c0 100644 --- a/spec/unit/knife/user_show_spec.rb +++ b/spec/unit/knife/user_show_spec.rb @@ -19,23 +19,47 @@ require 'spec_helper' describe Chef::Knife::UserShow do - before(:each) do + let(:knife) { Chef::Knife::UserShow.new } + let(:user_mock) { double('user_mock') } + let(:stdout) { StringIO.new } + + before do Chef::Knife::UserShow.load_deps - @knife = Chef::Knife::UserShow.new - @knife.name_args = [ 'my_user' ] - @user_mock = double('user_mock') + knife.name_args = [ 'my_user' ] + allow(user_mock).to receive(:username).and_return('my_user') + allow(knife.ui).to receive(:stderr).and_return(stdout) + allow(knife.ui).to receive(:stdout).and_return(stdout) + end + + # delete this once OSC11 support is gone + context "when the username field is not supported by the server" do + before do + allow(knife).to receive(:run_osc_11_user_show).and_raise(SystemExit) + allow(Chef::UserV1).to receive(:load).with('my_user').and_return(user_mock) + allow(user_mock).to receive(:username).and_return(nil) + end + + it "displays the osc warning" do + expect(knife.ui).to receive(:warn).with(knife.osc_11_warning) + expect{ knife.run }.to raise_error(SystemExit) + end + + it "forwards the command to knife osc_user edit" do + expect(knife).to receive(:run_osc_11_user_show) + expect{ knife.run }.to raise_error(SystemExit) + end end it 'loads and displays the user' do - expect(Chef::User).to receive(:load).with('my_user').and_return(@user_mock) - expect(@knife).to receive(:format_for_display).with(@user_mock) - @knife.run + expect(Chef::UserV1).to receive(:load).with('my_user').and_return(user_mock) + expect(knife).to receive(:format_for_display).with(user_mock) + knife.run end it 'prints usage and exits when a user name is not provided' do - @knife.name_args = [] - expect(@knife).to receive(:show_usage) - expect(@knife.ui).to receive(:fatal) - expect { @knife.run }.to raise_error(SystemExit) + knife.name_args = [] + expect(knife).to receive(:show_usage) + expect(knife.ui).to receive(:fatal) + expect { knife.run }.to raise_error(SystemExit) end end diff --git a/spec/unit/knife_spec.rb b/spec/unit/knife_spec.rb index 2ccf8493ad..022256f370 100644 --- a/spec/unit/knife_spec.rb +++ b/spec/unit/knife_spec.rb @@ -30,11 +30,20 @@ describe Chef::Knife do let(:knife) { Chef::Knife.new } + let(:config_location) { File.expand_path("~/.chef/config.rb") } + + let(:config_loader) do + instance_double("WorkstationConfigLoader", load: nil, no_config_found?: false, config_location: config_location) + end + before(:each) do Chef::Log.logger = Logger.new(StringIO.new) Chef::Config[:node_name] = "webmonkey.example.com" + allow(Chef::WorkstationConfigLoader).to receive(:new).and_return(config_loader) + allow(config_loader).to receive(:explicit_config_file=) + # Prevent gratuitous code reloading: allow(Chef::Knife).to receive(:load_commands) allow(knife.ui).to receive(:puts) @@ -130,7 +139,8 @@ describe Chef::Knife do "Accept-Encoding"=>"gzip;q=1.0,deflate;q=0.6,identity;q=0.3", 'X-Chef-Version' => Chef::VERSION, "Host"=>"api.opscode.piab", - "X-REMOTE-REQUEST-ID"=>request_id}} + "X-REMOTE-REQUEST-ID"=>request_id, + 'X-Ops-Server-API-Version' => Chef::HTTP::Authenticator::DEFAULT_SERVER_API_VERSION}} let(:request_id) {"1234"} @@ -251,6 +261,18 @@ describe Chef::Knife do :default => "default-value") end + it "sets the default log_location to STDERR for Chef::Log warnings" do + knife_command = KnifeSpecs::TestYourself.new([]) + knife_command.configure_chef + expect(Chef::Config[:log_location]).to eq(STDERR) + end + + it "sets the default log_level to warn so we can issue Chef::Log.warn" do + knife_command = KnifeSpecs::TestYourself.new([]) + knife_command.configure_chef + expect(Chef::Config[:log_level]).to eql(:warn) + end + it "prefers the default value if no config or command line value is present" do knife_command = KnifeSpecs::TestYourself.new([]) #empty argv knife_command.configure_chef @@ -271,6 +293,11 @@ describe Chef::Knife do expect(knife_command.config[:opt_with_default]).to eq("from-cli") end + it "merges `listen` config to Chef::Config" do + Chef::Knife.run(%w[test yourself --no-listen], Chef::Application::Knife.options) + expect(Chef::Config[:listen]).to be(false) + end + context "verbosity is greater than zero" do let(:fake_config) { "/does/not/exist/knife.rb" } @@ -369,6 +396,22 @@ describe Chef::Knife do expect(stderr.string).to match(%r[Response: nothing to see here]) end + it "formats 406s (non-supported API version error) nicely" do + response = Net::HTTPNotAcceptable.new("1.1", "406", "Not Acceptable") + response.instance_variable_set(:@read, true) # I hate you, net/http. + + # set the header + response["x-ops-server-api-version"] = Chef::JSONCompat.to_json(:min_version => "0", :max_version => "1", :request_version => "10000000") + + allow(response).to receive(:body).and_return(Chef::JSONCompat.to_json(:error => "sad trombone")) + allow(knife).to receive(:run).and_raise(Net::HTTPServerException.new("406 Not Acceptable", response)) + + knife.run_with_pretty_exceptions + expect(stderr.string).to include('The request that Knife sent was using API version 10000000') + expect(stderr.string).to include('The Chef server you sent the request to supports a min API verson of 0 and a max API version of 1') + expect(stderr.string).to include('Please either update your Chef client or server to be a compatible set') + end + it "formats 500s nicely" do response = Net::HTTPInternalServerError.new("1.1", "500", "Internal Server Error") response.instance_variable_set(:@read, true) # I hate you, net/http. diff --git a/spec/unit/log/syslog_spec.rb b/spec/unit/log/syslog_spec.rb new file mode 100644 index 0000000000..3db90e50c6 --- /dev/null +++ b/spec/unit/log/syslog_spec.rb @@ -0,0 +1,53 @@ +# +# Author:: SAWANOBORI Yukihiko (<sawanoboriyu@higanworks.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' + +describe "Chef::Log::Syslog", :unix_only => true do + let(:syslog) { Chef::Log::Syslog.new } + let(:app) { Chef::Application.new } + + before do + Chef::Log.init(MonoLogger.new(syslog)) + @old_log_level = Chef::Log.level + Chef::Log.level = :info + @old_loggers = Chef::Log.loggers + Chef::Log.use_log_devices([syslog]) + end + + after do + Chef::Log.level = @old_log_level + Chef::Log.use_log_devices(@old_loggers) + end + + it "should send message with severity info to syslog." do + expect(syslog).to receive(:info).with("*** Chef 12.4.0.dev.0 ***") + Chef::Log.info("*** Chef 12.4.0.dev.0 ***") + end + + it "should send message with severity warning to syslog." do + expect(syslog).to receive(:warn).with("No config file found or specified on command line, using command line options.") + Chef::Log.warn("No config file found or specified on command line, using command line options.") + end + + it "should fallback into send message with severity info to syslog when wrong format." do + expect(syslog).to receive(:info).with("chef message") + syslog.write("chef message") + end +end diff --git a/spec/unit/log/winevt_spec.rb b/spec/unit/log/winevt_spec.rb new file mode 100644 index 0000000000..867ef55900 --- /dev/null +++ b/spec/unit/log/winevt_spec.rb @@ -0,0 +1,55 @@ +# +# Author:: Jay Mundrawala (jdm@chef.io) +# Author:: SAWANOBORI Yukihiko (<sawanoboriyu@higanworks.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' + +describe Chef::Log::WinEvt do + let(:evtlog) { instance_double("Win32::EventLog")} + let(:winevt) { Chef::Log::WinEvt.new(evtlog) } + let(:app) { Chef::Application.new } + + before do + + Chef::Log.init(MonoLogger.new(winevt)) + @old_log_level = Chef::Log.level + Chef::Log.level = :info + @old_loggers = Chef::Log.loggers + Chef::Log.use_log_devices([winevt]) + end + + after do + Chef::Log.level = @old_log_level + Chef::Log.use_log_devices(@old_loggers) + end + + it "should send message with severity info to Windows Event Log." do + expect(winevt).to receive(:info).with("*** Chef 12.4.0.dev.0 ***") + Chef::Log.info("*** Chef 12.4.0.dev.0 ***") + end + + it "should send message with severity warning to Windows Event Log." do + expect(winevt).to receive(:warn).with("No config file found or specified on command line, using command line options.") + Chef::Log.warn("No config file found or specified on command line, using command line options.") + end + + it "should fallback into send message with severity info to Windows Event Log when wrong format." do + expect(winevt).to receive(:info).with("chef message") + winevt.write("chef message") + end +end diff --git a/spec/unit/lwrp_spec.rb b/spec/unit/lwrp_spec.rb index ec39174da6..bcb64cb21e 100644 --- a/spec/unit/lwrp_spec.rb +++ b/spec/unit/lwrp_spec.rb @@ -17,20 +17,40 @@ # require 'spec_helper' +require 'tmpdir' +require 'fileutils' +require 'chef/mixin/convert_to_class_name' module LwrpConstScopingConflict end describe "LWRP" do + include Chef::Mixin::ConvertToClassName + before do @original_VERBOSE = $VERBOSE $VERBOSE = nil + Chef::Resource::LWRPBase.class_eval { @loaded_lwrps = {} } end after do $VERBOSE = @original_VERBOSE end + def get_lwrp(name) + Chef::ResourceResolver.resolve(name) + end + + def get_lwrp_provider(name) + old_treat_deprecation_warnings_as_errors = Chef::Config[:treat_deprecation_warnings_as_errors] + Chef::Config[:treat_deprecation_warnings_as_errors] = false + begin + Chef::Provider.const_get(convert_to_class_name(name.to_s)) + ensure + Chef::Config[:treat_deprecation_warnings_as_errors] = old_treat_deprecation_warnings_as_errors + end + end + describe "when overriding an existing class" do before :each do allow($stderr).to receive(:write) @@ -43,7 +63,6 @@ describe "LWRP" do expect(Chef::Log).not_to receive(:debug).with(/anymore/) Chef::Resource::LWRPBase.build_from_file("lwrp", file, nil) Object.send(:remove_const, 'LwrpFoo') - Chef::Resource.send(:remove_const, 'LwrpFoo') end it "should not skip loading a provider when there's a top level symbol of the same name" do @@ -53,7 +72,6 @@ describe "LWRP" do expect(Chef::Log).not_to receive(:debug).with(/anymore/) Chef::Provider::LWRPBase.build_from_file("lwrp", file, nil) Object.send(:remove_const, 'LwrpBuckPasser') - Chef::Provider.send(:remove_const, 'LwrpBuckPasser') end # @todo: we need a before block to manually remove_const all of the LWRPs that we @@ -67,7 +85,6 @@ describe "LWRP" do Dir[File.expand_path( "lwrp/resources/*", CHEF_SPEC_DATA)].each do |file| expect(Chef::Log).to receive(:info).with(/Skipping/) - expect(Chef::Log).to receive(:debug).with(/anymore/) Chef::Resource::LWRPBase.build_from_file("lwrp", file, nil) end end @@ -79,7 +96,6 @@ describe "LWRP" do Dir[File.expand_path( "lwrp/providers/*", CHEF_SPEC_DATA)].each do |file| expect(Chef::Log).to receive(:info).with(/Skipping/) - expect(Chef::Log).to receive(:debug).with(/anymore/) Chef::Provider::LWRPBase.build_from_file("lwrp", file, nil) end end @@ -90,7 +106,7 @@ describe "LWRP" do Dir[File.expand_path( "lwrp/resources/*", CHEF_SPEC_DATA)].each do |file| Chef::Resource::LWRPBase.build_from_file("lwrp", file, nil) end - first_lwr_foo_class = Chef::Resource::LwrpFoo + first_lwr_foo_class = get_lwrp(:lwrp_foo) expect(Chef::Resource.resource_classes).to include(first_lwr_foo_class) Dir[File.expand_path( "lwrp/resources/*", CHEF_SPEC_DATA)].each do |file| Chef::Resource::LWRPBase.build_from_file("lwrp", file, nil) @@ -106,40 +122,91 @@ describe "LWRP" do end - describe "Lightweight Chef::Resource" do + context "When an LWRP resource in cookbook l-w-r-p is loaded" do + before do + @tmpdir = Dir.mktmpdir("lwrp_test") + resource_path = File.join(@tmpdir, "foo.rb") + IO.write(resource_path, "default_action :create") + provider_path = File.join(@tmpdir, "foo.rb") + IO.write(provider_path, <<-EOM) + action :create do + raise "hi" + end + EOM + end + + it "Can find the resource at l_w_r_p_foo" do + end + end + context "When an LWRP resource lwrp_foo is loaded" do before do - Dir[File.expand_path(File.join(File.dirname(__FILE__), "..", "data", "lwrp", "resources", "*"))].each do |file| - Chef::Resource::LWRPBase.build_from_file("lwrp", file, nil) + @tmpdir = Dir.mktmpdir("lwrp_test") + @lwrp_path = File.join(@tmpdir, "foo.rb") + content = IO.read(File.expand_path("../../data/lwrp/resources/foo.rb", __FILE__)) + IO.write(@lwrp_path, content) + Chef::Resource::LWRPBase.build_from_file("lwrp", @lwrp_path, nil) + @original_resource = Chef::ResourceResolver.resolve(:lwrp_foo) + end + + after do + FileUtils.remove_entry @tmpdir + end + + context "And the LWRP is asked to load again, this time with different code" do + before do + content = IO.read(File.expand_path("../../data/lwrp_override/resources/foo.rb", __FILE__)) + IO.write(@lwrp_path, content) + Chef::Resource::LWRPBase.build_from_file("lwrp", @lwrp_path, nil) + end + + it "Should load the old content, and not the new" do + resource = Chef::ResourceResolver.resolve(:lwrp_foo) + expect(resource).to eq @original_resource + expect(resource.default_action).to eq([:pass_buck]) + expect(Chef.method_defined?(:method_created_by_override_lwrp_foo)).to be_falsey end + end + end + + describe "Lightweight Chef::Resource" do - Dir[File.expand_path(File.join(File.dirname(__FILE__), "..", "data", "lwrp_override", "resources", "*"))].each do |file| + before do + Dir[File.expand_path(File.join(File.dirname(__FILE__), "..", "data", "lwrp", "resources", "*"))].each do |file| Chef::Resource::LWRPBase.build_from_file("lwrp", file, nil) end end - it "should load the resource into a properly-named class" do - expect(Chef::Resource.const_get("LwrpFoo")).to be_kind_of(Class) + it "should be resolvable with Chef::ResourceResolver.resolve(:lwrp_foo)" do + expect(Chef::ResourceResolver.resolve(:lwrp_foo, node: Chef::Node.new)).to eq(get_lwrp(:lwrp_foo)) end it "should set resource_name" do - expect(Chef::Resource::LwrpFoo.new("blah").resource_name).to eql(:lwrp_foo) + expect(get_lwrp(:lwrp_foo).new("blah").resource_name).to eql(:lwrp_foo) + end + + it "should output the resource_name in .to_s" do + expect(get_lwrp(:lwrp_foo).new("blah").to_s).to eq "lwrp_foo[blah]" + end + + it "should have a class that outputs a reasonable string" do + expect(get_lwrp(:lwrp_foo).to_s).to eq "LWRP resource lwrp_foo from cookbook lwrp" end it "should add the specified actions to the allowed_actions array" do - expect(Chef::Resource::LwrpFoo.new("blah").allowed_actions).to include(:pass_buck, :twiddle_thumbs) + expect(get_lwrp(:lwrp_foo).new("blah").allowed_actions).to include(:pass_buck, :twiddle_thumbs) end it "should set the specified action as the default action" do - expect(Chef::Resource::LwrpFoo.new("blah").action).to eq(:pass_buck) + expect(get_lwrp(:lwrp_foo).new("blah").action).to eq([:pass_buck]) end it "should create a method for each attribute" do - expect(Chef::Resource::LwrpFoo.new("blah").methods.map{ |m| m.to_sym}).to include(:monkey) + expect(get_lwrp(:lwrp_foo).new("blah").methods.map{ |m| m.to_sym}).to include(:monkey) end it "should build attribute methods that respect validation rules" do - expect { Chef::Resource::LwrpFoo.new("blah").monkey(42) }.to raise_error(ArgumentError) + expect { get_lwrp(:lwrp_foo).new("blah").monkey(42) }.to raise_error(ArgumentError) end it "should have access to the run context and node during class definition" do @@ -151,7 +218,7 @@ describe "LWRP" do Chef::Resource::LWRPBase.build_from_file("lwrp", file, run_context) end - cls = Chef::Resource.const_get("LwrpNodeattr") + cls = get_lwrp(:lwrp_nodeattr) expect(cls.node).to be_kind_of(Chef::Node) expect(cls.run_context).to be_kind_of(Chef::RunContext) expect(cls.node[:penguin_name]).to eql("jackass") @@ -175,14 +242,6 @@ describe "LWRP" do expect(klass.resource_name).to eq(:foo) end - context "when creating a new instance" do - it "raises an exception if resource_name is nil" do - expect { - klass.new('blah') - }.to raise_error(Chef::Exceptions::InvalidResourceSpecification) - end - end - context "lazy default values" do let(:klass) do Class.new(Chef::Resource::LWRPBase) do @@ -225,17 +284,17 @@ describe "LWRP" do end end - context "when the child does not defined the methods" do + context "when the child does not define the methods" do let(:child) do Class.new(parent) end it "delegates #actions to the parent" do - expect(child.actions).to eq([:eat, :sleep]) + expect(child.actions).to eq([:nothing, :eat, :sleep]) end it "delegates #default_action to the parent" do - expect(child.default_action).to eq(:eat) + expect(child.default_action).to eq([:eat]) end end @@ -248,11 +307,11 @@ describe "LWRP" do end it "does not delegate #actions to the parent" do - expect(child.actions).to eq([:dont_eat, :dont_sleep]) + expect(child.actions).to eq([:nothing, :dont_eat, :dont_sleep]) end it "does not delegate #default_action to the parent" do - expect(child.default_action).to eq(:dont_eat) + expect(child.default_action).to eq([:dont_eat]) end end @@ -273,110 +332,193 @@ describe "LWRP" do it "amends actions when they are already defined" do raise_if_deprecated! - expect(child.actions).to eq([:eat, :sleep, :drink]) + expect(child.actions).to eq([:nothing, :eat, :sleep, :drink]) + end + end + end + + describe "when actions is set to an array" do + let(:resource_class) do + Class.new(Chef::Resource::LWRPBase) do + actions [ :eat, :sleep ] end end + let(:resource) do + resource_class.new('blah') + end + it "actions includes those actions" do + expect(resource_class.actions).to eq [ :nothing, :eat, :sleep ] + end + it "allowed_actions includes those actions" do + expect(resource_class.allowed_actions).to eq [ :nothing, :eat, :sleep ] + end + it "resource.allowed_actions includes those actions" do + expect(resource.allowed_actions).to eq [ :nothing, :eat, :sleep ] + end end + describe "when allowed_actions is set to an array" do + let(:resource_class) do + Class.new(Chef::Resource::LWRPBase) do + allowed_actions [ :eat, :sleep ] + end + end + let(:resource) do + resource_class.new('blah') + end + it "actions includes those actions" do + expect(resource_class.actions).to eq [ :nothing, :eat, :sleep ] + end + it "allowed_actions includes those actions" do + expect(resource_class.allowed_actions).to eq [ :nothing, :eat, :sleep ] + end + it "resource.allowed_actions includes those actions" do + expect(resource.allowed_actions).to eq [ :nothing, :eat, :sleep ] + end + end end describe "Lightweight Chef::Provider" do - before do - @node = Chef::Node.new - @node.automatic[:platform] = :ubuntu - @node.automatic[:platform_version] = '8.10' - @events = Chef::EventDispatch::Dispatcher.new - @run_context = Chef::RunContext.new(@node, Chef::CookbookCollection.new({}), @events) - @runner = Chef::Runner.new(@run_context) - end - before(:each) do - Dir[File.expand_path(File.join(File.dirname(__FILE__), "..", "data", "lwrp", "resources", "*"))].each do |file| - Chef::Resource::LWRPBase.build_from_file("lwrp", file, @run_context) + let(:node) do + Chef::Node.new.tap do |n| + n.automatic[:platform] = :ubuntu + n.automatic[:platform_version] = '8.10' end + end - Dir[File.expand_path(File.join(File.dirname(__FILE__), "..", "data", "lwrp_override", "resources", "*"))].each do |file| - Chef::Resource::LWRPBase.build_from_file("lwrp", file, @run_context) - end + let(:events) { Chef::EventDispatch::Dispatcher.new } - Dir[File.expand_path(File.join(File.dirname(__FILE__), "..", "data", "lwrp", "providers", "*"))].each do |file| - Chef::Provider::LWRPBase.build_from_file("lwrp", file, @run_context) - end + let(:run_context) { Chef::RunContext.new(node, Chef::CookbookCollection.new({}), events) } + + let(:runner) { Chef::Runner.new(run_context) } + + let(:lwrp_cookbok_name) { "lwrp" } + + before do + Chef::Provider::LWRPBase.class_eval { @loaded_lwrps = {} } + end - Dir[File.expand_path(File.join(File.dirname(__FILE__), "..", "data", "lwrp_override", "providers", "*"))].each do |file| - Chef::Provider::LWRPBase.build_from_file("lwrp", file, @run_context) + before(:each) do + Dir[File.expand_path(File.expand_path("../../data/lwrp/resources/*", __FILE__))].each do |file| + Chef::Resource::LWRPBase.build_from_file(lwrp_cookbok_name, file, run_context) end + Dir[File.expand_path(File.expand_path("../../data/lwrp/providers/*", __FILE__))].each do |file| + Chef::Provider::LWRPBase.build_from_file(lwrp_cookbok_name, file, run_context) + end end it "should properly handle a new_resource reference" do - resource = Chef::Resource::LwrpFoo.new("morpheus") + resource = get_lwrp(:lwrp_foo).new("morpheus", run_context) resource.monkey("bob") - resource.provider(:lwrp_monkey_name_printer) - resource.run_context = @run_context + resource.provider(get_lwrp_provider(:lwrp_monkey_name_printer)) provider = Chef::Platform.provider_for_resource(resource, :twiddle_thumbs) provider.action_twiddle_thumbs end - it "should load the provider into a properly-named class" do - expect(Chef::Provider.const_get("LwrpBuckPasser")).to be_kind_of(Class) - end + context "provider class created" do + before do + @old_treat_deprecation_warnings_as_errors = Chef::Config[:treat_deprecation_warnings_as_errors] + Chef::Config[:treat_deprecation_warnings_as_errors] = false + end - it "should create a method for each attribute" do - new_resource = double("new resource").as_null_object - expect(Chef::Provider::LwrpBuckPasser.new(nil, new_resource).methods.map{|m|m.to_sym}).to include(:action_pass_buck) - expect(Chef::Provider::LwrpThumbTwiddler.new(nil, new_resource).methods.map{|m|m.to_sym}).to include(:action_twiddle_thumbs) + after do + Chef::Config[:treat_deprecation_warnings_as_errors] = @old_treat_deprecation_warnings_as_errors + end + + it "should load the provider into a properly-named class" do + expect(Chef::Provider.const_get("LwrpBuckPasser")).to be_kind_of(Class) + expect(Chef::Provider::LwrpBuckPasser <= Chef::Provider::LWRPBase).to be_truthy + end + + it "should create a method for each action" do + expect(get_lwrp_provider(:lwrp_buck_passer).instance_methods).to include(:action_pass_buck) + expect(get_lwrp_provider(:lwrp_thumb_twiddler).instance_methods).to include(:action_twiddle_thumbs) + end + + it "sets itself as a provider for a resource of the same name" do + found_providers = Chef::Platform::ProviderHandlerMap.instance.list(node, :lwrp_buck_passer) + # we bypass the per-file loading to get the file to load each time, + # which creates the LWRP class repeatedly. New things get prepended to + # the list of providers. + expect(found_providers.first).to eq(get_lwrp_provider(:lwrp_buck_passer)) + end + + context "with a cookbook with an underscore in the name" do + + let(:lwrp_cookbok_name) { "l_w_r_p" } + + it "sets itself as a provider for a resource of the same name" do + found_providers = Chef::Platform::ProviderHandlerMap.instance.list(node, :l_w_r_p_buck_passer) + expect(found_providers.size).to eq(1) + expect(found_providers.last).to eq(get_lwrp_provider(:l_w_r_p_buck_passer)) + end + end + + context "with a cookbook with a hypen in the name" do + + let(:lwrp_cookbok_name) { "l-w-r-p" } + + it "sets itself as a provider for a resource of the same name" do + incorrect_providers = Chef::Platform::ProviderHandlerMap.instance.list(node, :'l-w-r-p_buck_passer') + expect(incorrect_providers).to eq([]) + + found_providers = Chef::Platform::ProviderHandlerMap.instance.list(node, :l_w_r_p_buck_passer) + expect(found_providers.first).to eq(get_lwrp_provider(:l_w_r_p_buck_passer)) + end + end end it "should insert resources embedded in the provider into the middle of the resource collection" do - injector = Chef::Resource::LwrpFoo.new("morpheus", @run_context) + injector = get_lwrp(:lwrp_foo).new("morpheus", run_context) injector.action(:pass_buck) - injector.provider(:lwrp_buck_passer) - dummy = Chef::Resource::ZenMaster.new("keanu reeves", @run_context) + injector.provider(get_lwrp_provider(:lwrp_buck_passer)) + dummy = Chef::Resource::ZenMaster.new("keanu reeves", run_context) dummy.provider(Chef::Provider::Easy) - @run_context.resource_collection.insert(injector) - @run_context.resource_collection.insert(dummy) + run_context.resource_collection.insert(injector) + run_context.resource_collection.insert(dummy) - Chef::Runner.new(@run_context).converge + Chef::Runner.new(run_context).converge - expect(@run_context.resource_collection[0]).to eql(injector) - expect(@run_context.resource_collection[1].name).to eql('prepared_thumbs') - expect(@run_context.resource_collection[2].name).to eql('twiddled_thumbs') - expect(@run_context.resource_collection[3]).to eql(dummy) + expect(run_context.resource_collection[0]).to eql(injector) + expect(run_context.resource_collection[1].name).to eql('prepared_thumbs') + expect(run_context.resource_collection[2].name).to eql('twiddled_thumbs') + expect(run_context.resource_collection[3]).to eql(dummy) end it "should insert embedded resources from multiple providers, including from the last position, properly into the resource collection" do - injector = Chef::Resource::LwrpFoo.new("morpheus", @run_context) + injector = get_lwrp(:lwrp_foo).new("morpheus", run_context) injector.action(:pass_buck) - injector.provider(:lwrp_buck_passer) + injector.provider(get_lwrp_provider(:lwrp_buck_passer)) - injector2 = Chef::Resource::LwrpBar.new("tank", @run_context) + injector2 = get_lwrp(:lwrp_bar).new("tank", run_context) injector2.action(:pass_buck) - injector2.provider(:lwrp_buck_passer_2) + injector2.provider(get_lwrp_provider(:lwrp_buck_passer_2)) - dummy = Chef::Resource::ZenMaster.new("keanu reeves", @run_context) + dummy = Chef::Resource::ZenMaster.new("keanu reeves", run_context) dummy.provider(Chef::Provider::Easy) - @run_context.resource_collection.insert(injector) - @run_context.resource_collection.insert(dummy) - @run_context.resource_collection.insert(injector2) + run_context.resource_collection.insert(injector) + run_context.resource_collection.insert(dummy) + run_context.resource_collection.insert(injector2) - Chef::Runner.new(@run_context).converge + Chef::Runner.new(run_context).converge - expect(@run_context.resource_collection[0]).to eql(injector) - expect(@run_context.resource_collection[1].name).to eql('prepared_thumbs') - expect(@run_context.resource_collection[2].name).to eql('twiddled_thumbs') - expect(@run_context.resource_collection[3]).to eql(dummy) - expect(@run_context.resource_collection[4]).to eql(injector2) - expect(@run_context.resource_collection[5].name).to eql('prepared_eyes') - expect(@run_context.resource_collection[6].name).to eql('dried_paint_watched') + expect(run_context.resource_collection[0]).to eql(injector) + expect(run_context.resource_collection[1].name).to eql('prepared_thumbs') + expect(run_context.resource_collection[2].name).to eql('twiddled_thumbs') + expect(run_context.resource_collection[3]).to eql(dummy) + expect(run_context.resource_collection[4]).to eql(injector2) + expect(run_context.resource_collection[5].name).to eql('prepared_eyes') + expect(run_context.resource_collection[6].name).to eql('dried_paint_watched') end it "should properly handle a new_resource reference" do - resource = Chef::Resource::LwrpFoo.new("morpheus", @run_context) + resource = get_lwrp(:lwrp_foo).new("morpheus", run_context) resource.monkey("bob") - resource.provider(:lwrp_monkey_name_printer) + resource.provider(get_lwrp_provider(:lwrp_monkey_name_printer)) provider = Chef::Platform.provider_for_resource(resource, :twiddle_thumbs) provider.action_twiddle_thumbs @@ -385,9 +527,9 @@ describe "LWRP" do end it "should properly handle an embedded Resource accessing the enclosing Provider's scope" do - resource = Chef::Resource::LwrpFoo.new("morpheus", @run_context) + resource = get_lwrp(:lwrp_foo).new("morpheus", run_context) resource.monkey("bob") - resource.provider(:lwrp_embedded_resource_accesses_providers_scope) + resource.provider(get_lwrp_provider(:lwrp_embedded_resource_accesses_providers_scope)) provider = Chef::Platform.provider_for_resource(resource, :twiddle_thumbs) #provider = @runner.build_provider(resource) @@ -404,15 +546,15 @@ describe "LWRP" do # Side effect of lwrp_inline_compiler provider for testing notifications. $interior_ruby_block_2 = nil # resource type doesn't matter, so make an existing resource type work with provider. - @resource = Chef::Resource::LwrpFoo.new("morpheus", @run_context) + @resource = get_lwrp(:lwrp_foo).new("morpheus", run_context) @resource.allowed_actions << :test @resource.action(:test) - @resource.provider(:lwrp_inline_compiler) + @resource.provider(get_lwrp_provider(:lwrp_inline_compiler)) end it "does not add interior resources to the exterior resource collection" do @resource.run_action(:test) - expect(@run_context.resource_collection).to be_empty + expect(run_context.resource_collection).to be_empty end context "when interior resources are updated" do @@ -437,7 +579,144 @@ describe "LWRP" do end end - end + context "resource class created" do + before(:context) do + @tmpdir = Dir.mktmpdir("lwrp_test") + resource_path = File.join(@tmpdir, "once.rb") + IO.write(resource_path, "default_action :create") + + @old_treat_deprecation_warnings_as_errors = Chef::Config[:treat_deprecation_warnings_as_errors] + Chef::Config[:treat_deprecation_warnings_as_errors] = false + Chef::Resource::LWRPBase.build_from_file("lwrp", resource_path, nil) + end + + after(:context) do + FileUtils.remove_entry @tmpdir + Chef::Config[:treat_deprecation_warnings_as_errors] = @old_treat_deprecation_warnings_as_errors + end + + it "should load the resource into a properly-named class" do + expect(Chef::Resource::LwrpOnce).to be_kind_of(Class) + expect(Chef::Resource::LwrpOnce <= Chef::Resource::LWRPBase).to be_truthy + end + + it "get_lwrp(:lwrp_once).new is a Chef::Resource::LwrpOnce" do + lwrp = get_lwrp(:lwrp_once).new('hi') + expect(lwrp.kind_of?(Chef::Resource::LwrpOnce)).to be_truthy + expect(lwrp.is_a?(Chef::Resource::LwrpOnce)).to be_truthy + expect(get_lwrp(:lwrp_once) === lwrp).to be_truthy + expect(Chef::Resource::LwrpOnce === lwrp).to be_truthy + end + + it "Chef::Resource::LwrpOnce.new is a get_lwrp(:lwrp_once)" do + lwrp = Chef::Resource::LwrpOnce.new('hi') + expect(lwrp.kind_of?(get_lwrp(:lwrp_once))).to be_truthy + expect(lwrp.is_a?(get_lwrp(:lwrp_once))).to be_truthy + expect(get_lwrp(:lwrp_once) === lwrp).to be_truthy + expect(Chef::Resource::LwrpOnce === lwrp).to be_truthy + end + + it "works even if LwrpOnce exists in the top level" do + module ::LwrpOnce + end + expect(Chef::Resource::LwrpOnce).not_to eq(::LwrpOnce) + end + + it "allows monkey patching of the lwrp through Chef::Resource" do + monkey = Module.new do + def issue_3607 + end + end + Chef::Resource::LwrpOnce.send(:include, monkey) + expect { get_lwrp(:lwrp_once).new("blah").issue_3607 }.not_to raise_error + end + + context "with a subclass of get_lwrp(:lwrp_once)" do + let(:subclass) do + Class.new(get_lwrp(:lwrp_once)) + end + + it "subclass.new is a subclass" do + lwrp = subclass.new('hi') + expect(lwrp.kind_of?(subclass)).to be_truthy + expect(lwrp.is_a?(subclass)).to be_truthy + expect(subclass === lwrp).to be_truthy + expect(lwrp.class === subclass) + end + it "subclass.new is a Chef::Resource::LwrpOnce" do + lwrp = subclass.new('hi') + expect(lwrp.kind_of?(Chef::Resource::LwrpOnce)).to be_truthy + expect(lwrp.is_a?(Chef::Resource::LwrpOnce)).to be_truthy + expect(Chef::Resource::LwrpOnce === lwrp).to be_truthy + expect(lwrp.class === Chef::Resource::LwrpOnce) + end + it "subclass.new is a get_lwrp(:lwrp_once)" do + lwrp = subclass.new('hi') + expect(lwrp.kind_of?(get_lwrp(:lwrp_once))).to be_truthy + expect(lwrp.is_a?(get_lwrp(:lwrp_once))).to be_truthy + expect(get_lwrp(:lwrp_once) === lwrp).to be_truthy + expect(lwrp.class === get_lwrp(:lwrp_once)) + end + it "Chef::Resource::LwrpOnce.new is *not* a subclass" do + lwrp = Chef::Resource::LwrpOnce.new('hi') + expect(lwrp.kind_of?(subclass)).to be_falsey + expect(lwrp.is_a?(subclass)).to be_falsey + expect(subclass === lwrp.class).to be_falsey + expect(subclass === Chef::Resource::LwrpOnce).to be_falsey + end + it "get_lwrp(:lwrp_once).new is *not* a subclass" do + lwrp = get_lwrp(:lwrp_once).new('hi') + expect(lwrp.kind_of?(subclass)).to be_falsey + expect(lwrp.is_a?(subclass)).to be_falsey + expect(subclass === lwrp.class).to be_falsey + expect(subclass === get_lwrp(:lwrp_once)).to be_falsey + end + end + + context "with a subclass of Chef::Resource::LwrpOnce" do + let(:subclass) do + Class.new(Chef::Resource::LwrpOnce) + end + + it "subclass.new is a subclass" do + lwrp = subclass.new('hi') + expect(lwrp.kind_of?(subclass)).to be_truthy + expect(lwrp.is_a?(subclass)).to be_truthy + expect(subclass === lwrp).to be_truthy + expect(lwrp.class === subclass) + end + it "subclass.new is a Chef::Resource::LwrpOnce" do + lwrp = subclass.new('hi') + expect(lwrp.kind_of?(Chef::Resource::LwrpOnce)).to be_truthy + expect(lwrp.is_a?(Chef::Resource::LwrpOnce)).to be_truthy + expect(Chef::Resource::LwrpOnce === lwrp).to be_truthy + expect(lwrp.class === Chef::Resource::LwrpOnce) + end + it "subclass.new is a get_lwrp(:lwrp_once)" do + lwrp = subclass.new('hi') + expect(lwrp.kind_of?(get_lwrp(:lwrp_once))).to be_truthy + expect(lwrp.is_a?(get_lwrp(:lwrp_once))).to be_truthy + expect(get_lwrp(:lwrp_once) === lwrp).to be_truthy + expect(lwrp.class === get_lwrp(:lwrp_once)) + end + it "Chef::Resource::LwrpOnce.new is *not* a subclass" do + lwrp = Chef::Resource::LwrpOnce.new('hi') + expect(lwrp.kind_of?(subclass)).to be_falsey + expect(lwrp.is_a?(subclass)).to be_falsey + expect(subclass === lwrp.class).to be_falsey + expect(subclass === Chef::Resource::LwrpOnce).to be_falsey + end + it "get_lwrp(:lwrp_once).new is *not* a subclass" do + lwrp = get_lwrp(:lwrp_once).new('hi') + expect(lwrp.kind_of?(subclass)).to be_falsey + expect(lwrp.is_a?(subclass)).to be_falsey + expect(subclass === lwrp.class).to be_falsey + expect(subclass === get_lwrp(:lwrp_once)).to be_falsey + end + end + end end + + diff --git a/spec/unit/mixin/api_version_request_handling_spec.rb b/spec/unit/mixin/api_version_request_handling_spec.rb new file mode 100644 index 0000000000..cc5340e424 --- /dev/null +++ b/spec/unit/mixin/api_version_request_handling_spec.rb @@ -0,0 +1,127 @@ +# +# Author:: Tyler Cloke (tyler@chef.io) +# Copyright:: Copyright 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' + +describe Chef::Mixin::ApiVersionRequestHandling do + let(:dummy_class) { Class.new { include Chef::Mixin::ApiVersionRequestHandling } } + let(:object) { dummy_class.new } + + describe ".server_client_api_version_intersection" do + let(:default_supported_client_versions) { [0,1,2] } + + + context "when the response code is not 406" do + let(:response) { OpenStruct.new(:code => '405') } + let(:exception) { Net::HTTPServerException.new("405 Something Else", response) } + + it "returns nil" do + expect(object.server_client_api_version_intersection(exception, default_supported_client_versions)). + to be_nil + end + + end # when the response code is not 406 + + context "when the response code is 406" do + let(:response) { OpenStruct.new(:code => '406') } + let(:exception) { Net::HTTPServerException.new("406 Not Acceptable", response) } + + context "when x-ops-server-api-version header does not exist" do + it "returns nil" do + expect(object.server_client_api_version_intersection(exception, default_supported_client_versions)). + to be_nil + end + end # when x-ops-server-api-version header does not exist + + context "when x-ops-server-api-version header exists" do + let(:min_server_version) { 2 } + let(:max_server_version) { 4 } + let(:return_hash) { + { + "min_version" => min_server_version, + "max_version" => max_server_version + } + } + + before(:each) do + allow(response).to receive(:[]).with('x-ops-server-api-version').and_return(Chef::JSONCompat.to_json(return_hash)) + end + + context "when there is no intersection between client and server versions" do + shared_examples_for "no intersection between client and server versions" do + it "return an array" do + expect(object.server_client_api_version_intersection(exception, supported_client_versions)). + to be_a_kind_of(Array) + end + + it "returns an empty array" do + expect(object.server_client_api_version_intersection(exception, supported_client_versions).length). + to eq(0) + end + + end + + context "when all the versions are higher than the max" do + it_should_behave_like "no intersection between client and server versions" do + let(:supported_client_versions) { [5,6,7] } + end + end + + context "when all the versions are lower than the min" do + it_should_behave_like "no intersection between client and server versions" do + let(:supported_client_versions) { [0,1] } + end + end + + end # when there is no intersection between client and server versions + + context "when there is an intersection between client and server versions" do + context "when multiple versions intersect" do + let(:supported_client_versions) { [1,2,3,4,5] } + + it "includes all of the intersection" do + expect(object.server_client_api_version_intersection(exception, supported_client_versions)). + to eq([2,3,4]) + end + end # when multiple versions intersect + + context "when only the min client version intersects" do + let(:supported_client_versions) { [0,1,2] } + + it "includes the intersection" do + expect(object.server_client_api_version_intersection(exception, supported_client_versions)). + to eq([2]) + end + end # when only the min client version intersects + + context "when only the max client version intersects" do + let(:supported_client_versions) { [4,5,6] } + + it "includes the intersection" do + expect(object.server_client_api_version_intersection(exception, supported_client_versions)). + to eq([4]) + end + end # when only the max client version intersects + + end # when there is an intersection between client and server versions + + end # when x-ops-server-api-version header exists + end # when the response code is 406 + + end # .server_client_api_version_intersection +end # Chef::Mixin::ApiVersionRequestHandling diff --git a/spec/unit/mixin/command_spec.rb b/spec/unit/mixin/command_spec.rb index e198e3addd..050b261256 100644 --- a/spec/unit/mixin/command_spec.rb +++ b/spec/unit/mixin/command_spec.rb @@ -22,7 +22,7 @@ describe Chef::Mixin::Command, :volatile do if windows? - pending("TODO MOVE: this is a platform specific integration test.") + skip("TODO MOVE: this is a platform specific integration test.") else @@ -61,7 +61,6 @@ describe Chef::Mixin::Command, :volatile do it "returns immediately after the first child process exits" do expect {Timeout.timeout(10) do - pid, stdin,stdout,stderr = nil,nil,nil,nil evil_forker="exit if fork; 10.times { sleep 1}" popen4("ruby -e '#{evil_forker}'") do |pid,stdin,stdout,stderr| end diff --git a/spec/unit/mixin/params_validate_spec.rb b/spec/unit/mixin/params_validate_spec.rb index 1b61f9b238..3724bbf583 100644 --- a/spec/unit/mixin/params_validate_spec.rb +++ b/spec/unit/mixin/params_validate_spec.rb @@ -21,6 +21,8 @@ require 'spec_helper' class TinyClass include Chef::Mixin::ParamsValidate + attr_reader :name + def music(is_good=true) is_good end @@ -331,91 +333,77 @@ describe Chef::Mixin::ParamsValidate do it "asserts that a value returns false from a predicate method" do expect do @vo.validate({:not_blank => "should pass"}, - {:not_blank => {:cannot_be => :nil, :cannot_be => :empty}}) + {:not_blank => {:cannot_be => [ :nil, :empty ]}}) end.not_to raise_error expect do @vo.validate({:not_blank => ""}, - {:not_blank => {:cannot_be => :nil, :cannot_be => :empty}}) + {:not_blank => {:cannot_be => [ :nil, :empty ]}}) end.to raise_error(Chef::Exceptions::ValidationFailed) end - def self.test_set_or_return_method(method) - # caller is responsible for passing in the right arg to get 'return' behavior - return_arg = method == :nillable_set_or_return ? TinyClass::NULL_ARG : nil - - it "#{method} should set and return a value, then return the same value" do - value = "meow" - expect(@vo.send(method,:test, value, {}).object_id).to eq(value.object_id) - expect(@vo.send(method,:test, return_arg, {}).object_id).to eq(value.object_id) - end - - it "#{method} should set and return a default value when the argument is nil, then return the same value" do - value = "meow" - expect(@vo.send(method,:test, return_arg, { :default => value }).object_id).to eq(value.object_id) - expect(@vo.send(method,:test, return_arg, {}).object_id).to eq(value.object_id) - end - - it "#{method} should raise an ArgumentError when argument is nil and required is true" do - expect { - @vo.send(method,:test, return_arg, { :required => true }) - }.to raise_error(ArgumentError) - end - - it "#{method} should not raise an error when argument is nil and required is false" do - expect { - @vo.send(method,:test, return_arg, { :required => false }) - }.not_to raise_error - end - - it "#{method} should set and return @name, then return @name for foo when argument is nil" do - value = "meow" - expect(@vo.send(method,:name, value, { }).object_id).to eq(value.object_id) - expect(@vo.send(method,:foo, return_arg, { :name_attribute => true }).object_id).to eq(value.object_id) - end - - it "#{method} should allow DelayedEvaluator instance to be set for value regardless of restriction" do - value = Chef::DelayedEvaluator.new{ 'test' } - @vo.send(method,:test, value, {:kind_of => Numeric}) - end - - it "#{method} should raise an error when delayed evaluated attribute is not valid" do - value = Chef::DelayedEvaluator.new{ 'test' } - @vo.send(method,:test, value, {:kind_of => Numeric}) - expect do - @vo.send(method,:test, return_arg, {:kind_of => Numeric}) - end.to raise_error(Chef::Exceptions::ValidationFailed) - end - - it "#{method} should create DelayedEvaluator instance when #lazy is used" do - @vo.send(method,:delayed, @vo.lazy{ 'test' }, {}) - expect(@vo.instance_variable_get(:@delayed)).to be_a(Chef::DelayedEvaluator) - end - - it "#{method} should execute block on each call when DelayedEvaluator" do - value = 'fubar' - @vo.send(method,:test, @vo.lazy{ value }, {}) - expect(@vo.send(method,:test, return_arg, {})).to eq('fubar') - value = 'foobar' - expect(@vo.send(method,:test, return_arg, {})).to eq('foobar') - value = 'fauxbar' - expect(@vo.send(method,:test, return_arg, {})).to eq('fauxbar') - end - - it "#{method} should not evaluate non DelayedEvaluator instances" do - value = lambda{ 'test' } - @vo.send(method,:test, value, {}) - expect(@vo.send(method,:test, return_arg, {}).object_id).to eq(value.object_id) - expect(@vo.send(method,:test, return_arg, {})).to be_a(Proc) - end + it "should set and return a value, then return the same value" do + value = "meow" + expect(@vo.set_or_return(:test, value, {}).object_id).to eq(value.object_id) + expect(@vo.set_or_return(:test, nil, {}).object_id).to eq(value.object_id) + end + + it "should set and return a default value when the argument is nil, then return the same value" do + value = "meow" + expect(@vo.set_or_return(:test, nil, { :default => value }).object_id).to eq(value.object_id) + expect(@vo.set_or_return(:test, nil, {}).object_id).to eq(value.object_id) + end + + it "should raise an ArgumentError when argument is nil and required is true" do + expect { + @vo.set_or_return(:test, nil, { :required => true }) + }.to raise_error(ArgumentError) + end + + it "should not raise an error when argument is nil and required is false" do + expect { + @vo.set_or_return(:test, nil, { :required => false }) + }.not_to raise_error end - test_set_or_return_method(:set_or_return) - test_set_or_return_method(:nillable_set_or_return) + it "should set and return @name, then return @name for foo when argument is nil" do + value = "meow" + expect(@vo.set_or_return(:name, value, { }).object_id).to eq(value.object_id) + expect(@vo.set_or_return(:foo, nil, { :name_attribute => true }).object_id).to eq(value.object_id) + end - it "nillable_set_or_return supports nilling values" do - expect(@vo.nillable_set_or_return(:test, "meow", {})).to eq("meow") - expect(@vo.nillable_set_or_return(:test, TinyClass::NULL_ARG, {})).to eq("meow") - expect(@vo.nillable_set_or_return(:test, nil, {})).to be_nil - expect(@vo.nillable_set_or_return(:test, TinyClass::NULL_ARG, {})).to be_nil + it "should allow DelayedEvaluator instance to be set for value regardless of restriction" do + value = Chef::DelayedEvaluator.new{ 'test' } + @vo.set_or_return(:test, value, {:kind_of => Numeric}) end + + it "should raise an error when delayed evaluated attribute is not valid" do + value = Chef::DelayedEvaluator.new{ 'test' } + @vo.set_or_return(:test, value, {:kind_of => Numeric}) + expect do + @vo.set_or_return(:test, nil, {:kind_of => Numeric}) + end.to raise_error(Chef::Exceptions::ValidationFailed) + end + + it "should create DelayedEvaluator instance when #lazy is used" do + @vo.set_or_return(:delayed, @vo.lazy{ 'test' }, {}) + expect(@vo.instance_variable_get(:@delayed)).to be_a(Chef::DelayedEvaluator) + end + + it "should execute block on each call when DelayedEvaluator" do + value = 'fubar' + @vo.set_or_return(:test, @vo.lazy{ value }, {}) + expect(@vo.set_or_return(:test, nil, {})).to eq('fubar') + value = 'foobar' + expect(@vo.set_or_return(:test, nil, {})).to eq('foobar') + value = 'fauxbar' + expect(@vo.set_or_return(:test, nil, {})).to eq('fauxbar') + end + + it "should not evaluate non DelayedEvaluator instances" do + value = lambda{ 'test' } + @vo.set_or_return(:test, value, {}) + expect(@vo.set_or_return(:test, nil, {}).object_id).to eq(value.object_id) + expect(@vo.set_or_return(:test, nil, {})).to be_a(Proc) + end + end diff --git a/spec/unit/mixin/path_sanity_spec.rb b/spec/unit/mixin/path_sanity_spec.rb index ec8e182e3d..3a924b9538 100644 --- a/spec/unit/mixin/path_sanity_spec.rb +++ b/spec/unit/mixin/path_sanity_spec.rb @@ -35,7 +35,7 @@ describe Chef::Mixin::PathSanity do @gem_bindir = '/some/gem/bin' allow(Gem).to receive(:bindir).and_return(@gem_bindir) allow(RbConfig::CONFIG).to receive(:[]).with('bindir').and_return(@ruby_bindir) - allow(Chef::Platform).to receive(:windows?).and_return(false) + allow(ChefConfig).to receive(:windows?).and_return(false) end it "adds all useful PATHs even if environment is an empty hash" do @@ -77,7 +77,7 @@ describe Chef::Mixin::PathSanity do gem_bindir = 'C:\gems\bin' allow(Gem).to receive(:bindir).and_return(gem_bindir) allow(RbConfig::CONFIG).to receive(:[]).with('bindir').and_return(ruby_bindir) - allow(Chef::Platform).to receive(:windows?).and_return(true) + allow(ChefConfig).to receive(:windows?).and_return(true) env = {"PATH" => 'C:\Windows\system32;C:\mr\softie'} @sanity.enforce_path_sanity(env) expect(env["PATH"]).to eq("C:\\Windows\\system32;C:\\mr\\softie;#{ruby_bindir};#{gem_bindir}") diff --git a/spec/unit/mixin/powershell_out_spec.rb b/spec/unit/mixin/powershell_out_spec.rb new file mode 100644 index 0000000000..0fede582fa --- /dev/null +++ b/spec/unit/mixin/powershell_out_spec.rb @@ -0,0 +1,70 @@ +# +# 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/mixin/powershell_out' + +describe Chef::Mixin::PowershellOut do + let(:shell_out_class) { Class.new { include Chef::Mixin::PowershellOut } } + subject(:object) { shell_out_class.new } + let(:architecture) { "something" } + let(:flags) { + "-NoLogo -NonInteractive -NoProfile -ExecutionPolicy Unrestricted -InputFormat None" + } + + describe "#powershell_out" do + it "runs a command and returns the shell_out object" do + ret = double("Mixlib::ShellOut") + expect(object).to receive(:shell_out).with( + "powershell.exe #{flags} -Command \"Get-Process\"", + {} + ).and_return(ret) + expect(object.powershell_out("Get-Process")).to eql(ret) + end + + it "passes options" do + ret = double("Mixlib::ShellOut") + expect(object).to receive(:shell_out).with( + "powershell.exe #{flags} -Command \"Get-Process\"", + timeout: 600 + ).and_return(ret) + expect(object.powershell_out("Get-Process", timeout: 600)).to eql(ret) + end + end + + describe "#powershell_out!" do + it "runs a command and returns the shell_out object" do + mixlib_shellout = double("Mixlib::ShellOut") + expect(object).to receive(:shell_out).with( + "powershell.exe #{flags} -Command \"Get-Process\"", + {} + ).and_return(mixlib_shellout) + expect(mixlib_shellout).to receive(:error!) + expect(object.powershell_out!("Get-Process")).to eql(mixlib_shellout) + end + + it "passes options" do + mixlib_shellout = double("Mixlib::ShellOut") + expect(object).to receive(:shell_out).with( + "powershell.exe #{flags} -Command \"Get-Process\"", + timeout: 600 + ).and_return(mixlib_shellout) + expect(mixlib_shellout).to receive(:error!) + expect(object.powershell_out!("Get-Process", timeout: 600)).to eql(mixlib_shellout) + end + end +end diff --git a/spec/unit/mixin/powershell_type_coercions_spec.rb b/spec/unit/mixin/powershell_type_coercions_spec.rb new file mode 100644 index 0000000000..988c3926c1 --- /dev/null +++ b/spec/unit/mixin/powershell_type_coercions_spec.rb @@ -0,0 +1,72 @@ +# +# Author:: Jay Mundrawala (<jdm@chef.io>) +# 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/mixin/powershell_type_coercions' +require 'base64' + +class Chef::PSTypeTester + include Chef::Mixin::PowershellTypeCoercions +end + +describe Chef::Mixin::PowershellTypeCoercions do + let (:test_class) { Chef::PSTypeTester.new } + + describe '#translate_type' do + it 'should single quote a string' do + expect(test_class.translate_type('foo')).to eq("'foo'") + end + + ["'", '"', '#', '`'].each do |c| + it "should base64 encode a string that contains #{c}" do + expect(test_class.translate_type("#{c}")).to match(Base64.strict_encode64(c)) + end + end + + it 'should not quote an integer' do + expect(test_class.translate_type(123)).to eq('123') + end + + it 'should not quote a floating point number' do + expect(test_class.translate_type(123.4)).to eq('123.4') + end + + it 'should return $false when an instance of FalseClass is provided' do + expect(test_class.translate_type(false)).to eq('$false') + end + + it 'should return $true when an instance of TrueClass is provided' do + expect(test_class.translate_type(true)).to eq('$true') + end + + it 'should translate all members of a hash and wrap them in @{} separated by ;' do + expect(test_class.translate_type({"a" => 1, "b" => 1.2, "c" => false, "d" => true + })).to eq("@{a=1;b=1.2;c=$false;d=$true}") + end + + it 'should translat all members of an array and them by a ,' do + expect(test_class.translate_type([true, false])).to eq('@($true,$false)') + end + + it 'should fall back :to_psobject if we have not defined at explicit rule' do + ps_obj = double("PSObject") + expect(ps_obj).to receive(:to_psobject).and_return('$true') + expect(test_class.translate_type(ps_obj)).to eq('($true)') + end + end +end diff --git a/spec/unit/mixin/template_spec.rb b/spec/unit/mixin/template_spec.rb index f02bd34b8f..6a867b5f9a 100644 --- a/spec/unit/mixin/template_spec.rb +++ b/spec/unit/mixin/template_spec.rb @@ -39,7 +39,7 @@ describe Chef::Mixin::Template, "render_template" do describe "when running on windows" do before do - allow(Chef::Platform).to receive(:windows?).and_return(true) + allow(ChefConfig).to receive(:windows?).and_return(true) end it "should render the templates with windows line endings" do @@ -54,7 +54,7 @@ describe Chef::Mixin::Template, "render_template" do describe "when running on unix" do before do - allow(Chef::Platform).to receive(:windows?).and_return(false) + allow(ChefConfig).to receive(:windows?).and_return(false) end it "should render the templates with unix line endings" do diff --git a/spec/unit/mixin/unformatter_spec.rb b/spec/unit/mixin/unformatter_spec.rb new file mode 100644 index 0000000000..2eae0ac9bb --- /dev/null +++ b/spec/unit/mixin/unformatter_spec.rb @@ -0,0 +1,61 @@ +# +# Author:: Jay Mundrawala (<jdm@chef.io>) +# Copyright:: Copyright (c) 2015 Chef Software +# 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/mixin/unformatter' + +class Chef::UnformatterTest + include Chef::Mixin::Unformatter + + def foo + end + +end + +describe Chef::Mixin::Unformatter do + let (:unformatter) { Chef::UnformatterTest.new } + let (:message) { "Test Message" } + + describe "#write" do + context "with a timestamp" do + it "sends foo to itself when the message is of severity foo" do + expect(unformatter).to receive(:foo).with(message) + unformatter.write("[time] foo: #{message}") + end + + it "sends foo to itself when the message is of severity FOO" do + expect(unformatter).to receive(:foo).with(message) + unformatter.write("[time] FOO: #{message}") + end + end + + context "without a timestamp" do + it "sends foo to itself when the message is of severity foo" do + expect(unformatter).to receive(:foo).with(message) + unformatter.write("foo: #{message}") + end + + it "sends foo to itself when the message is of severity FOO" do + expect(unformatter).to receive(:foo).with(message) + unformatter.write("FOO: #{message}") + end + end + + end + +end diff --git a/spec/unit/mixin/uris_spec.rb b/spec/unit/mixin/uris_spec.rb new file mode 100644 index 0000000000..d4985c4f67 --- /dev/null +++ b/spec/unit/mixin/uris_spec.rb @@ -0,0 +1,57 @@ +# +# Author:: Jay Mundrawala (<jdm@chef.io>) +# 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/mixin/uris' + +class Chef::UrisTest + include Chef::Mixin::Uris +end + +describe Chef::Mixin::Uris do + let (:uris) { Chef::UrisTest.new } + + describe "#uri_scheme?" do + it "matches 'scheme://foo.com'" do + expect(uris.uri_scheme?('scheme://foo.com')).to eq(true) + end + + it "does not match 'c:/foo.com'" do + expect(uris.uri_scheme?('c:/foo.com')).to eq(false) + end + + it "does not match '/usr/bin/foo.com'" do + expect(uris.uri_scheme?('/usr/bin/foo.com')).to eq(false) + end + + it "does not match 'c:/foo.com://bar.com'" do + expect(uris.uri_scheme?('c:/foo.com://bar.com')).to eq(false) + end + end + + describe "#as_uri" do + it "parses a file scheme uri with spaces" do + expect{ uris.as_uri("file:///c:/foo bar.txt") }.not_to raise_exception + end + + it "returns a URI object" do + expect( uris.as_uri("file:///c:/foo bar.txt") ).to be_a(URI) + end + end + +end diff --git a/spec/unit/mixin/windows_architecture_helper_spec.rb b/spec/unit/mixin/windows_architecture_helper_spec.rb index 3803d69371..55eca28dc2 100644 --- a/spec/unit/mixin/windows_architecture_helper_spec.rb +++ b/spec/unit/mixin/windows_architecture_helper_spec.rb @@ -60,23 +60,28 @@ describe Chef::Mixin::WindowsArchitectureHelper do end end - it "returns true for each supported desired architecture for all nodes with each valid architecture passed to node_supports_windows_architecture" do - enumerate_architecture_node_combinations(true) + it "returns true only for supported desired architecture passed to node_supports_windows_architecture" do + with_node_architecture_combinations do | node, desired_arch | + expect(node_supports_windows_architecture?(node, desired_arch)).to be true if (node_windows_architecture(node) == :x86_64 || desired_arch == :i386 ) + expect(node_supports_windows_architecture?(node, desired_arch)).to be false if (node_windows_architecture(node) == :i386 && desired_arch == :x86_64 ) + end end - it "returns false for each unsupported desired architecture for all nodes with each valid architecture passed to node_supports_windows_architecture?" do - enumerate_architecture_node_combinations(true) + it "returns true only when forced_32bit_override_required? has 64-bit node architecture and 32-bit desired architecture" do + with_node_architecture_combinations do | node, desired_arch | + expect(forced_32bit_override_required?(node, desired_arch)).to be true if ((node_windows_architecture(node) == :x86_64) && (desired_arch == :i386) && !is_i386_process_on_x86_64_windows?) + expect(forced_32bit_override_required?(node, desired_arch)).to be false if ! ((node_windows_architecture(node) == :x86_64) && (desired_arch == :i386)) + end end - def enumerate_architecture_node_combinations(only_valid_combinations) + def with_node_architecture_combinations @valid_architectures.each do | node_architecture | new_node = Chef::Node.new new_node.default["kernel"] = Hash.new new_node.default["kernel"][:machine] = node_architecture.to_s - @valid_architectures.each do | supported_architecture | - expect(node_supports_windows_architecture?(new_node, supported_architecture)).to eq(true) if only_valid_combinations && (supported_architecture != :x86_64 && node_architecture != :i386 ) - expect(node_supports_windows_architecture?(new_node, supported_architecture)).to eq(false) if ! only_valid_combinations && (supported_architecture == :x86_64 && node_architecture == :i386 ) + @valid_architectures.each do | architecture | + yield new_node, architecture if block_given? end end end diff --git a/spec/unit/node_map_spec.rb b/spec/unit/node_map_spec.rb index fe7372961b..7b37ea59f4 100644 --- a/spec/unit/node_map_spec.rb +++ b/spec/unit/node_map_spec.rb @@ -131,9 +131,25 @@ describe Chef::NodeMap do allow(node).to receive(:[]).with(:platform_version).and_return("6.0") expect(node_map.get(node, :thing)).to eql(nil) end + + context "when there is a less specific definition" do + before do + node_map.set(:thing, :bar, platform_family: "rhel") + end + + it "returns the value when the node matches" do + allow(node).to receive(:[]).with(:platform_family).and_return("rhel") + allow(node).to receive(:[]).with(:platform_version).and_return("7.0") + expect(node_map.get(node, :thing)).to eql(:foo) + end + end end describe "resource back-compat testing" do + before :each do + Chef::Config[:treat_deprecation_warnings_as_errors] = false + end + it "should handle :on_platforms => :all" do node_map.set(:chef_gem, :foo, :on_platforms => :all) allow(node).to receive(:[]).with(:platform).and_return("windows") @@ -152,4 +168,3 @@ describe Chef::NodeMap do end end - diff --git a/spec/unit/node_spec.rb b/spec/unit/node_spec.rb index 5939403ce6..b7752eb734 100644 --- a/spec/unit/node_spec.rb +++ b/spec/unit/node_spec.rb @@ -672,6 +672,13 @@ describe Chef::Node do expect(node.run_list).to eq([ "role[base]", "recipe[chef::server]" ]) end + it "sets the node chef_environment" do + attrs = { "chef_environment" => "foo_environment", "bar" => "baz" } + expect(node.consume_chef_environment(attrs)).to eq({ "bar" => "baz" }) + expect(node.chef_environment).to eq("foo_environment") + expect(node['chef_environment']).to be nil + end + it "should overwrites the run list with the run list it consumes" do node.consume_run_list "recipes" => [ "one", "two" ] node.consume_run_list "recipes" => [ "three" ] @@ -1106,7 +1113,7 @@ describe Chef::Node do expect(serialized_node.run_list).to eq(node.run_list) end - include_examples "to_json equalivent to Chef::JSONCompat.to_json" do + include_examples "to_json equivalent to Chef::JSONCompat.to_json" do let(:jsonable) { node.from_file(File.expand_path("nodes/test.example.com.rb", CHEF_SPEC_DATA)) node diff --git a/spec/unit/platform/query_helpers_spec.rb b/spec/unit/platform/query_helpers_spec.rb index 7aafc287ea..33d4c2c3b7 100644 --- a/spec/unit/platform/query_helpers_spec.rb +++ b/spec/unit/platform/query_helpers_spec.rb @@ -20,7 +20,7 @@ require 'spec_helper' describe "Chef::Platform#windows_server_2003?" do it "returns false early when not on windows" do - allow(Chef::Platform).to receive(:windows?).and_return(false) + allow(ChefConfig).to receive(:windows?).and_return(false) expect(Chef::Platform).not_to receive(:require) expect(Chef::Platform.windows_server_2003?).to be_falsey end @@ -53,3 +53,25 @@ describe 'Chef::Platform#supports_dsc?' do end end end + +describe 'Chef::Platform#supports_dsc_invoke_resource?' do + it 'returns false if powershell is not present' do + node = Chef::Node.new + expect(Chef::Platform.supports_dsc_invoke_resource?(node)).to be_falsey + end + + ['1.0', '2.0', '3.0', '4.0', '5.0.10017.9'].each do |version| + it "returns false for Powershell #{version}" do + node = Chef::Node.new + node.automatic[:languages][:powershell][:version] = version + expect(Chef::Platform.supports_dsc_invoke_resource?(node)).to be_falsey + end + end + + it "returns true for Powershell 5.0.10018.0" do + node = Chef::Node.new + node.automatic[:languages][:powershell][:version] = "5.0.10018.0" + expect(Chef::Platform.supports_dsc_invoke_resource?(node)).to be_truthy + end +end + diff --git a/spec/unit/platform_spec.rb b/spec/unit/platform_spec.rb index fb65ef0fea..34b46f657f 100644 --- a/spec/unit/platform_spec.rb +++ b/spec/unit/platform_spec.rb @@ -18,34 +18,6 @@ require 'spec_helper' -describe "Chef::Platform supports" do - [ - :mac_os_x, - :mac_os_x_server, - :freebsd, - :ubuntu, - :debian, - :centos, - :fedora, - :suse, - :opensuse, - :redhat, - :oracle, - :gentoo, - :arch, - :solaris, - :mswin, - :mingw32, - :windows, - :gcel, - :ibm_powerkvm - ].each do |platform| - it "#{platform}" do - expect(Chef::Platform.platforms).to have_key(platform) - end - end -end - describe Chef::Platform do context "while testing with fake data" do @@ -131,7 +103,7 @@ describe Chef::Platform do end it "should raise an exception if a provider cannot be found for a resource type" do - expect { Chef::Platform.find_provider("Darwin", "9.2.2", :coffee) }.to raise_error(ArgumentError) + expect { Chef::Platform.find_provider("Darwin", "9.2.2", :coffee) }.to raise_error(Chef::Exceptions::ProviderNotFound) end it "should look up a provider for a resource with a Chef::Resource object" do @@ -266,41 +238,4 @@ describe Chef::Platform do end - context "while testing the configured platform data" do - - it "should use the solaris package provider on Solaris <11" do - pmap = Chef::Platform.find("Solaris2", "5.9") - expect(pmap[:package]).to eql(Chef::Provider::Package::Solaris) - end - - it "should use the IPS package provider on Solaris 11" do - pmap = Chef::Platform.find("Solaris2", "5.11") - expect(pmap[:package]).to eql(Chef::Provider::Package::Ips) - end - - it "should use the Redhat service provider on SLES11" do - 1.upto(3) do |sp| - pmap = Chef::Platform.find("SUSE", "11.#{sp}") - expect(pmap[:service]).to eql(Chef::Provider::Service::Redhat) - end - end - - it "should use the Systemd service provider on SLES12" do - pmap = Chef::Platform.find("SUSE", "12.0") - expect(pmap[:service]).to eql(Chef::Provider::Service::Systemd) - end - - it "should use the SUSE group provider on SLES11" do - 1.upto(3) do |sp| - pmap = Chef::Platform.find("SUSE", "11.#{sp}") - expect(pmap[:group]).to eql(Chef::Provider::Group::Suse) - end - end - - it "should use the Gpasswd group provider on SLES12" do - pmap = Chef::Platform.find("SUSE", "12.0") - expect(pmap[:group]).to eql(Chef::Provider::Group::Gpasswd) - end - end - end diff --git a/spec/unit/policy_builder/policyfile_spec.rb b/spec/unit/policy_builder/policyfile_spec.rb index 8b6e928a46..5fa00d8f2b 100644 --- a/spec/unit/policy_builder/policyfile_spec.rb +++ b/spec/unit/policy_builder/policyfile_spec.rb @@ -166,13 +166,17 @@ describe Chef::PolicyBuilder::Policyfile do end before do - # TODO: agree on this name and logic. + Chef::Config[:policy_document_native_api] = false Chef::Config[:deployment_group] = "example-policy-stage" allow(policy_builder).to receive(:http_api).and_return(http_api) end describe "when using compatibility mode (policy_document_native_api == false)" do + before do + Chef::Config[:deployment_group] = "example-policy-stage" + end + context "when the deployment group cannot be loaded" do let(:error404) { Net::HTTPServerException.new("404 message", :body) } @@ -256,7 +260,7 @@ describe Chef::PolicyBuilder::Policyfile do context "and policy_name and policy_group are configured" do - let(:policy_relative_url) { "policies/policy-stage/example" } + let(:policy_relative_url) { "policy_groups/policy-stage/policies/example" } before do expect(http_api).to receive(:get).with(policy_relative_url).and_return(parsed_policyfile_json) @@ -386,8 +390,11 @@ describe Chef::PolicyBuilder::Policyfile do describe "fetching the desired cookbook set" do - let(:example1_cookbook_object) { double("Chef::CookbookVersion for example1 cookbook") } - let(:example2_cookbook_object) { double("Chef::CookbookVersion for example2 cookbook") } + let(:example1_cookbook_data) { double("CookbookVersion Hash for example1 cookbook") } + let(:example2_cookbook_data) { double("CookbookVersion Hash for example2 cookbook") } + + let(:example1_cookbook_object) { double("Chef::CookbookVersion for example1 cookbook", version: "0.1.2") } + let(:example2_cookbook_object) { double("Chef::CookbookVersion for example2 cookbook", version: "1.2.3") } let(:expected_cookbook_hash) do { "example1" => example1_cookbook_object, "example2" => example2_cookbook_object } @@ -396,9 +403,12 @@ describe Chef::PolicyBuilder::Policyfile do let(:example1_xyz_version) { example1_lock_data["dotted_decimal_identifier"] } let(:example2_xyz_version) { example2_lock_data["dotted_decimal_identifier"] } + let(:example1_identifier) { example1_lock_data["identifier"] } + let(:example2_identifier) { example2_lock_data["identifier"] } + let(:cookbook_synchronizer) { double("Chef::CookbookSynchronizer") } - shared_examples_for "fetching cookbooks" do + shared_examples "fetching cookbooks when they don't exist" do context "and a cookbook is missing" do let(:error404) { Net::HTTPServerException.new("404 message", :body) } @@ -418,7 +428,9 @@ describe Chef::PolicyBuilder::Policyfile do end end + end + shared_examples_for "fetching cookbooks when they exist" do context "and the cookbooks can be fetched" do before do expect(Chef::Node).to receive(:find_or_create).with(node_name).and_return(node) @@ -426,11 +438,6 @@ describe Chef::PolicyBuilder::Policyfile do policy_builder.load_node policy_builder.build_node - expect(http_api).to receive(:get).with(cookbook1_url). - and_return(example1_cookbook_object) - expect(http_api).to receive(:get).with(cookbook2_url). - and_return(example2_cookbook_object) - allow(Chef::CookbookSynchronizer).to receive(:new). with(expected_cookbook_hash, events). and_return(cookbook_synchronizer) @@ -457,11 +464,23 @@ describe Chef::PolicyBuilder::Policyfile do end # shared_examples_for "fetching cookbooks" context "when using compatibility mode (policy_document_native_api == false)" do - include_examples "fetching cookbooks" do + let(:cookbook1_url) { "cookbooks/example1/#{example1_xyz_version}" } + let(:cookbook2_url) { "cookbooks/example2/#{example2_xyz_version}" } + + context "when the cookbooks don't exist on the server" do + include_examples "fetching cookbooks when they don't exist" + end - let(:cookbook1_url) { "cookbooks/example1/#{example1_xyz_version}" } - let(:cookbook2_url) { "cookbooks/example2/#{example2_xyz_version}" } + context "when the cookbooks exist on the server" do + before do + expect(http_api).to receive(:get).with(cookbook1_url). + and_return(example1_cookbook_object) + expect(http_api).to receive(:get).with(cookbook2_url). + and_return(example2_cookbook_object) + end + + include_examples "fetching cookbooks when they exist" end end @@ -474,13 +493,33 @@ describe Chef::PolicyBuilder::Policyfile do Chef::Config[:policy_name] = "example" end - include_examples "fetching cookbooks" do + let(:cookbook1_url) { "cookbook_artifacts/example1/#{example1_identifier}" } + let(:cookbook2_url) { "cookbook_artifacts/example2/#{example2_identifier}" } + + context "when the cookbooks don't exist on the server" do + include_examples "fetching cookbooks when they don't exist" + end + + + context "when the cookbooks exist on the server" do + + before do + expect(http_api).to receive(:get).with(cookbook1_url). + and_return(example1_cookbook_data) + expect(http_api).to receive(:get).with(cookbook2_url). + and_return(example2_cookbook_data) + + expect(Chef::CookbookVersion).to receive(:from_cb_artifact_data).with(example1_cookbook_data). + and_return(example1_cookbook_object) + expect(Chef::CookbookVersion).to receive(:from_cb_artifact_data).with(example2_cookbook_data). + and_return(example2_cookbook_object) + end - let(:cookbook1_url) { "cookbook_artifacts/example1/#{example1_xyz_version}" } - let(:cookbook2_url) { "cookbook_artifacts/example2/#{example2_xyz_version}" } + include_examples "fetching cookbooks when they exist" end + end end diff --git a/spec/unit/property/state_spec.rb b/spec/unit/property/state_spec.rb new file mode 100644 index 0000000000..e7fee0387f --- /dev/null +++ b/spec/unit/property/state_spec.rb @@ -0,0 +1,506 @@ +require 'support/shared/integration/integration_helper' + +describe "Chef::Resource#identity and #state" do + include IntegrationSupport + + class NewResourceNamer + @i = 0 + def self.next + "chef_resource_property_spec_#{@i += 1}" + end + end + + def self.new_resource_name + NewResourceNamer.next + end + + let(:resource_class) do + new_resource_name = self.class.new_resource_name + Class.new(Chef::Resource) do + resource_name new_resource_name + end + end + + let(:resource) do + resource_class.new("blah") + end + + def self.english_join(values) + return '<nothing>' if values.size == 0 + return values[0].inspect if values.size == 1 + "#{values[0..-2].map { |v| v.inspect }.join(", ")} and #{values[-1].inspect}" + end + + def self.with_property(*properties, &block) + tags_index = properties.find_index { |p| !p.is_a?(String)} + if tags_index + properties, tags = properties[0..tags_index-1], properties[tags_index..-1] + else + tags = [] + end + properties = properties.map { |property| "property #{property}" } + context "With properties #{english_join(properties)}", *tags do + before do + properties.each do |property_str| + resource_class.class_eval(property_str, __FILE__, __LINE__) + end + end + instance_eval(&block) + end + end + + # identity + context "Chef::Resource#identity_properties" do + with_property ":x" do + it "name is the default identity" do + expect(resource_class.identity_properties).to eq [ Chef::Resource.properties[:name] ] + expect(Chef::Resource.properties[:name].identity?).to be_falsey + expect(resource.name).to eq 'blah' + expect(resource.identity).to eq 'blah' + end + + it "identity_properties :x changes the identity" do + expect(resource_class.identity_properties :x).to eq [ resource_class.properties[:x] ] + expect(resource_class.identity_properties).to eq [ resource_class.properties[:x] ] + expect(Chef::Resource.properties[:name].identity?).to be_falsey + expect(resource_class.properties[:x].identity?).to be_truthy + + expect(resource.x 'woo').to eq 'woo' + expect(resource.x).to eq 'woo' + + expect(resource.name).to eq 'blah' + expect(resource.identity).to eq 'woo' + end + + with_property ":y, identity: true" do + context "and identity_properties :x" do + before do + resource_class.class_eval do + identity_properties :x + end + end + + it "only returns :x as identity" do + resource.x 'foo' + resource.y 'bar' + expect(resource_class.identity_properties).to eq [ resource_class.properties[:x] ] + expect(resource.identity).to eq 'foo' + end + it "does not flip y.desired_state off" do + resource.x 'foo' + resource.y 'bar' + expect(resource_class.state_properties).to eq [ + resource_class.properties[:x], + resource_class.properties[:y] + ] + expect(resource.state_for_resource_reporter).to eq(x: 'foo', y: 'bar') + end + end + end + + context "With a subclass" do + let(:subresource_class) do + new_resource_name = self.class.new_resource_name + Class.new(resource_class) do + resource_name new_resource_name + end + end + let(:subresource) do + subresource_class.new('sub') + end + + it "name is the default identity on the subclass" do + expect(subresource_class.identity_properties).to eq [ Chef::Resource.properties[:name] ] + expect(Chef::Resource.properties[:name].identity?).to be_falsey + expect(subresource.name).to eq 'sub' + expect(subresource.identity).to eq 'sub' + end + + context "With identity_properties :x on the superclass" do + before do + resource_class.class_eval do + identity_properties :x + end + end + + it "The subclass inherits :x as identity" do + expect(subresource_class.identity_properties).to eq [ subresource_class.properties[:x] ] + expect(Chef::Resource.properties[:name].identity?).to be_falsey + expect(subresource_class.properties[:x].identity?).to be_truthy + + subresource.x 'foo' + expect(subresource.identity).to eq 'foo' + end + + context "With property :y, identity: true on the subclass" do + before do + subresource_class.class_eval do + property :y, identity: true + end + end + it "The subclass's identity includes both x and y" do + expect(subresource_class.identity_properties).to eq [ + subresource_class.properties[:x], + subresource_class.properties[:y] + ] + subresource.x 'foo' + subresource.y 'bar' + expect(subresource.identity).to eq(x: 'foo', y: 'bar') + end + end + + with_property ":y, String" do + context "With identity_properties :y on the subclass" do + before do + subresource_class.class_eval do + identity_properties :y + end + end + it "y is part of state" do + subresource.x 'foo' + subresource.y 'bar' + expect(subresource.state_for_resource_reporter).to eq(x: 'foo', y: 'bar') + expect(subresource_class.state_properties).to eq [ + subresource_class.properties[:x], + subresource_class.properties[:y] + ] + end + it "y is the identity" do + expect(subresource_class.identity_properties).to eq [ subresource_class.properties[:y] ] + subresource.x 'foo' + subresource.y 'bar' + expect(subresource.identity).to eq 'bar' + end + it "y still has validation" do + expect { subresource.y 12 }.to raise_error Chef::Exceptions::ValidationFailed + end + end + end + end + end + end + + with_property ":string_only, String, identity: true", ":string_only2, String" do + it "identity_properties does not change validation" do + resource_class.identity_properties :string_only + expect { resource.string_only 12 }.to raise_error Chef::Exceptions::ValidationFailed + expect { resource.string_only2 12 }.to raise_error Chef::Exceptions::ValidationFailed + end + end + + with_property ":x, desired_state: false" do + it "identity_properties does not change desired_state" do + resource_class.identity_properties :x + resource.x 'hi' + expect(resource.identity).to eq 'hi' + expect(resource_class.properties[:x].desired_state?).to be_falsey + expect(resource_class.state_properties).to eq [] + expect(resource.state_for_resource_reporter).to eq({}) + end + end + + context "With custom property custom_property defined only as methods, using different variables for storage" do + before do + resource_class.class_eval do + def custom_property + @blarghle ? @blarghle*3 : nil + end + def custom_property=(x) + @blarghle = x*2 + end + end + end + + context "And identity_properties :custom_property" do + before do + resource_class.class_eval do + identity_properties :custom_property + end + end + + it "identity_properties comes back as :custom_property" do + expect(resource_class.properties[:custom_property].identity?).to be_truthy + expect(resource_class.identity_properties).to eq [ resource_class.properties[:custom_property] ] + end + it "custom_property becomes part of desired_state" do + resource.custom_property = 1 + expect(resource.state_for_resource_reporter).to eq(custom_property: 6) + expect(resource_class.properties[:custom_property].desired_state?).to be_truthy + expect(resource_class.state_properties).to eq [ + resource_class.properties[:custom_property] + ] + end + it "identity_properties does not change custom_property's getter or setter" do + resource.custom_property = 1 + expect(resource.custom_property).to eq 6 + end + it "custom_property is returned as the identity" do + expect(resource.identity).to be_nil + resource.custom_property = 1 + expect(resource.identity).to eq 6 + end + end + end + end + + context "Property#identity" do + with_property ":x, identity: true" do + it "name is only part of the identity if an identity attribute is defined" do + expect(resource_class.identity_properties).to eq [ resource_class.properties[:x] ] + resource.x 'woo' + expect(resource.identity).to eq 'woo' + end + end + + with_property ":x, identity: true, default: 'xxx'", + ":y, identity: true, default: 'yyy'", + ":z, identity: true, default: 'zzz'" do + it "identity_property raises an error if multiple identity values are defined" do + expect { resource_class.identity_property }.to raise_error Chef::Exceptions::MultipleIdentityError + end + it "identity_attr raises an error if multiple identity values are defined" do + expect { resource_class.identity_attr }.to raise_error Chef::Exceptions::MultipleIdentityError + end + it "identity returns all identity values in a hash if multiple are defined" do + resource.x 'foo' + resource.y 'bar' + resource.z 'baz' + expect(resource.identity).to eq(x: 'foo', y: 'bar', z: 'baz') + end + it "identity returns all values whether any value is set or not" do + expect(resource.identity).to eq(x: 'xxx', y: 'yyy', z: 'zzz') + end + it "identity_properties wipes out any other identity attributes if multiple are defined" do + resource_class.identity_properties :y + resource.x 'foo' + resource.y 'bar' + resource.z 'baz' + expect(resource.identity).to eq 'bar' + end + end + + with_property ":x, identity: true, name_property: true" do + it "identity when x is not defined returns the value of x" do + expect(resource.identity).to eq 'blah' + end + it "state when x is not defined returns the value of x" do + expect(resource.state_for_resource_reporter).to eq(x: 'blah') + end + end + end + + # state_properties + context "Chef::Resource#state_properties" do + it "state_properties is empty by default" do + expect(Chef::Resource.state_properties).to eq [] + expect(resource.state_for_resource_reporter).to eq({}) + end + + with_property ":x", ":y", ":z" do + it "x, y and z are state attributes" do + resource.x 1 + resource.y 2 + resource.z 3 + expect(resource_class.state_properties).to eq [ + resource_class.properties[:x], + resource_class.properties[:y], + resource_class.properties[:z] + ] + expect(resource.state_for_resource_reporter).to eq(x: 1, y: 2, z: 3) + end + it "values that are not set are not included in state" do + resource.x 1 + expect(resource.state_for_resource_reporter).to eq(x: 1) + end + it "when no values are set, nothing is included in state" do + end + end + + with_property ":x", ":y, desired_state: false", ":z, desired_state: true" do + it "x and z are state attributes, and y is not" do + resource.x 1 + resource.y 2 + resource.z 3 + expect(resource_class.state_properties).to eq [ + resource_class.properties[:x], + resource_class.properties[:z] + ] + expect(resource.state_for_resource_reporter).to eq(x: 1, z: 3) + end + end + + with_property ":x, name_property: true" do + # it "Unset values with name_property are included in state" do + # expect(resource.state_for_resource_reporter).to eq({ x: 'blah' }) + # end + it "Set values with name_property are included in state" do + resource.x 1 + expect(resource.state_for_resource_reporter).to eq(x: 1) + end + end + + with_property ":x, default: 1" do + it "Unset values with defaults are not included in state" do + expect(resource.state_for_resource_reporter).to eq({}) + end + it "Set values with defaults are included in state" do + resource.x 1 + expect(resource.state_for_resource_reporter).to eq(x: 1) + end + end + + context "With a class with a normal getter and setter" do + before do + resource_class.class_eval do + def x + @blah*3 + end + def x=(value) + @blah = value*2 + end + end + end + it "state_properties(:x) causes the value to be included in properties" do + resource_class.state_properties(:x) + resource.x = 1 + + expect(resource.x).to eq 6 + expect(resource.state_for_resource_reporter).to eq(x: 6) + end + end + + context "When state_properties happens before properties are declared" do + before do + resource_class.class_eval do + state_properties :x + property :x + end + end + it "the property works and is in state_properties" do + expect(resource_class.state_properties).to include(resource_class.properties[:x]) + resource.x = 1 + expect(resource.x).to eq 1 + expect(resource.state_for_resource_reporter).to eq(x: 1) + end + end + + with_property ":x, Integer, identity: true" do + it "state_properties(:x) leaves the property in desired_state" do + resource_class.state_properties(:x) + resource.x 10 + + expect(resource_class.properties[:x].desired_state?).to be_truthy + expect(resource_class.state_properties).to eq [ + resource_class.properties[:x] + ] + expect(resource.state_for_resource_reporter).to eq(x: 10) + end + it "state_properties(:x) does not turn off validation" do + resource_class.state_properties(:x) + expect { resource.x 'ouch' }.to raise_error Chef::Exceptions::ValidationFailed + end + it "state_properties(:x) does not turn off identity" do + resource_class.state_properties(:x) + resource.x 10 + + expect(resource_class.identity_properties).to eq [ resource_class.properties[:x] ] + expect(resource_class.properties[:x].identity?).to be_truthy + expect(resource.identity).to eq 10 + end + end + + with_property ":x, Integer, identity: true, desired_state: false" do + before do + resource_class.class_eval do + def y + 20 + end + end + end + + it "state_properties(:x) leaves x identical" do + old_value = resource_class.properties[:y] + resource_class.state_properties(:x) + resource.x 10 + + expect(resource_class.properties[:y].object_id).to eq old_value.object_id + + expect(resource_class.properties[:x].desired_state?).to be_truthy + expect(resource_class.properties[:x].identity?).to be_truthy + expect(resource_class.identity_properties).to eq [ + resource_class.properties[:x] + ] + expect(resource.identity).to eq(10) + expect(resource_class.state_properties).to eq [ + resource_class.properties[:x] + ] + expect(resource.state_for_resource_reporter).to eq(x: 10) + end + + it "state_properties(:y) adds y to desired state" do + old_value = resource_class.properties[:x] + resource_class.state_properties(:y) + resource.x 10 + + expect(resource_class.properties[:x].object_id).to eq old_value.object_id + expect(resource_class.properties[:x].desired_state?).to be_falsey + expect(resource_class.properties[:y].desired_state?).to be_truthy + expect(resource_class.state_properties).to eq [ + resource_class.properties[:y] + ] + expect(resource.state_for_resource_reporter).to eq(y: 20) + end + + context "With a subclassed resource" do + let(:subresource_class) do + new_resource_name = self.class.new_resource_name + Class.new(resource_class) do + resource_name new_resource_name + end + end + let(:subresource) do + subresource_class.new('blah') + end + + it "state_properties(:x) adds x to desired state" do + old_value = resource_class.properties[:y] + subresource_class.state_properties(:x) + subresource.x 10 + + expect(subresource_class.properties[:y].object_id).to eq old_value.object_id + + expect(subresource_class.properties[:x].desired_state?).to be_truthy + expect(subresource_class.properties[:x].identity?).to be_truthy + expect(subresource_class.identity_properties).to eq [ + subresource_class.properties[:x] + ] + expect(subresource.identity).to eq(10) + expect(subresource_class.state_properties).to eq [ + subresource_class.properties[:x] + ] + expect(subresource.state_for_resource_reporter).to eq(x: 10) + end + + it "state_properties(:y) adds y to desired state" do + old_value = resource_class.properties[:x] + subresource_class.state_properties(:y) + subresource.x 10 + + expect(subresource_class.properties[:x].object_id).to eq old_value.object_id + expect(subresource_class.properties[:y].desired_state?).to be_truthy + expect(subresource_class.state_properties).to eq [ + subresource_class.properties[:y] + ] + expect(subresource.state_for_resource_reporter).to eq(y: 20) + + expect(subresource_class.properties[:x].identity?).to be_truthy + expect(subresource_class.identity_properties).to eq [ + subresource_class.properties[:x] + ] + expect(subresource.identity).to eq(10) + end + end + end + end + +end diff --git a/spec/unit/property/validation_spec.rb b/spec/unit/property/validation_spec.rb new file mode 100644 index 0000000000..31bb3f0739 --- /dev/null +++ b/spec/unit/property/validation_spec.rb @@ -0,0 +1,663 @@ +require 'support/shared/integration/integration_helper' + +describe "Chef::Resource.property validation" do + include IntegrationSupport + + module Namer + @i = 0 + def self.next_resource_name + "chef_resource_property_spec_#{@i += 1}" + end + def self.reset_index + @current_index = 0 + end + def self.current_index + @current_index + end + def self.next_index + @current_index += 1 + end + end + + def lazy(&block) + Chef::DelayedEvaluator.new(&block) + end + + before do + Namer.reset_index + end + + def self.new_resource_name + Namer.next_resource_name + end + + let(:resource_class) do + new_resource_name = self.class.new_resource_name + Class.new(Chef::Resource) do + resource_name new_resource_name + def blah + Namer.next_index + end + def self.blah + "class#{Namer.next_index}" + end + end + end + + let(:resource) do + resource_class.new("blah") + end + + def self.english_join(values) + return '<nothing>' if values.size == 0 + return values[0].inspect if values.size == 1 + "#{values[0..-2].map { |v| v.inspect }.join(", ")} and #{values[-1].inspect}" + end + + def self.with_property(*properties, &block) + tags_index = properties.find_index { |p| !p.is_a?(String)} + if tags_index + properties, tags = properties[0..tags_index-1], properties[tags_index..-1] + else + tags = [] + end + properties = properties.map { |property| "property #{property}" } + context "With properties #{english_join(properties)}", *tags do + before do + properties.each do |property_str| + resource_class.class_eval(property_str, __FILE__, __LINE__) + end + end + instance_eval(&block) + end + end + + def self.validation_test(validation, success_values, failure_values, getter_values=[], *tags) + with_property ":x, #{validation}", *tags do + it "gets nil when retrieving the initial (non-set) value" do + expect(resource.x).to be_nil + end + success_values.each do |v| + it "value #{v.inspect} is valid" do + resource.instance_eval { @x = 'default' } + expect(resource.x v).to eq v + expect(resource.x).to eq v + end + end + failure_values.each do |v| + it "value #{v.inspect} is invalid" do + expect { resource.x v }.to raise_error Chef::Exceptions::ValidationFailed + resource.instance_eval { @x = 'default' } + expect { resource.x v }.to raise_error Chef::Exceptions::ValidationFailed + end + end + getter_values.each do |v| + it "setting value to #{v.inspect} does not change the value" do + Chef::Config[:treat_deprecation_warnings_as_errors] = false + resource.instance_eval { @x = 'default' } + expect(resource.x v).to eq 'default' + expect(resource.x).to eq 'default' + end + end + end + end + + context "basic get, set, and nil set" do + with_property ":x, kind_of: String" do + context "when the variable already has a value" do + before do + resource.instance_eval { @x = 'default' } + end + it "get succeeds" do + expect(resource.x).to eq 'default' + end + it "set to valid value succeeds" do + expect(resource.x 'str').to eq 'str' + expect(resource.x).to eq 'str' + end + it "set to invalid value raises ValidationFailed" do + expect { resource.x 10 }.to raise_error Chef::Exceptions::ValidationFailed + end + it "set to nil emits a deprecation warning and does a get" do + expect { resource.x nil }.to raise_error Chef::Exceptions::DeprecatedFeatureError + Chef::Config[:treat_deprecation_warnings_as_errors] = false + resource.x 'str' + expect(resource.x nil).to eq 'str' + expect(resource.x).to eq 'str' + end + end + context "when the variable does not have an initial value" do + it "get succeeds" do + expect(resource.x).to be_nil + end + it "set to valid value succeeds" do + expect(resource.x 'str').to eq 'str' + expect(resource.x).to eq 'str' + end + it "set to invalid value raises ValidationFailed" do + expect { resource.x 10 }.to raise_error Chef::Exceptions::ValidationFailed + end + it "set to nil emits a deprecation warning and does a get" do + expect { resource.x nil }.to raise_error Chef::Exceptions::DeprecatedFeatureError + Chef::Config[:treat_deprecation_warnings_as_errors] = false + resource.x 'str' + expect(resource.x nil).to eq 'str' + expect(resource.x).to eq 'str' + end + end + end + with_property ":x, [ String, nil ]" do + context "when the variable already has a value" do + before do + resource.instance_eval { @x = 'default' } + end + it "get succeeds" do + expect(resource.x).to eq 'default' + end + it "set(nil) sets the value" do + expect(resource.x nil).to be_nil + expect(resource.x).to be_nil + end + it "set to valid value succeeds" do + expect(resource.x 'str').to eq 'str' + expect(resource.x).to eq 'str' + end + it "set to invalid value raises ValidationFailed" do + expect { resource.x 10 }.to raise_error Chef::Exceptions::ValidationFailed + end + end + context "when the variable does not have an initial value" do + it "get succeeds" do + expect(resource.x).to be_nil + end + it "set(nil) sets the value" do + expect(resource.x nil).to be_nil + expect(resource.x).to be_nil + end + it "set to valid value succeeds" do + expect(resource.x 'str').to eq 'str' + expect(resource.x).to eq 'str' + end + it "set to invalid value raises ValidationFailed" do + expect { resource.x 10 }.to raise_error Chef::Exceptions::ValidationFailed + end + end + end + end + + # Bare types + context "bare types" do + validation_test 'String', + [ 'hi' ], + [ 10 ], + [ nil ] + + validation_test ':a', + [ :a ], + [ :b ], + [ nil ] + + validation_test ':a, is: :b', + [ :a, :b ], + [ :c ], + [ nil ] + + validation_test ':a, is: [ :b, :c ]', + [ :a, :b, :c ], + [ :d ], + [ nil ] + + validation_test '[ :a, :b ], is: :c', + [ :a, :b, :c ], + [ :d ], + [ nil ] + + validation_test '[ :a, :b ], is: [ :c, :d ]', + [ :a, :b, :c, :d ], + [ :e ], + [ nil ] + + validation_test 'nil', + [ nil ], + [ :a ] + + validation_test '[ nil ]', + [ nil ], + [ :a ] + + validation_test '[]', + [], + [ :a ], + [ nil ] + end + + # is + context "is" do + # Class + validation_test 'is: String', + [ 'a', '' ], + [ :a, 1 ], + [ nil ] + + # Value + validation_test 'is: :a', + [ :a ], + [ :b ], + [ nil ] + + validation_test 'is: [ :a, :b ]', + [ :a, :b ], + [ [ :a, :b ] ], + [ nil ] + + validation_test 'is: [ [ :a, :b ] ]', + [ [ :a, :b ] ], + [ :a, :b ], + [ nil ] + + # Regex + validation_test 'is: /abc/', + [ 'abc', 'wowabcwow' ], + [ '', 'abac' ], + [ nil ] + + # Property + validation_test 'is: Chef::Property.new(is: :a)', + [ :a ], + [ :b, nil ] + + # RSpec Matcher + class Globalses + extend RSpec::Matchers + end + + validation_test "is: Globalses.eq(10)", + [ 10 ], + [ 1 ], + [ nil ] + + # Proc + validation_test 'is: proc { |x| x }', + [ true, 1 ], + [ false ], + [ nil ] + + validation_test 'is: proc { |x| x > blah }', + [ 10 ], + [ -1 ] + + validation_test 'is: nil', + [ nil ], + [ 'a' ] + + validation_test 'is: [ String, nil ]', + [ 'a', nil ], + [ :b ] + + validation_test 'is: []', + [], + [ :a ], + [ nil ] + end + + # Combination + context "combination" do + validation_test 'kind_of: String, equal_to: "a"', + [ 'a' ], + [ 'b' ], + [ nil ] + end + + # equal_to + context "equal_to" do + # Value + validation_test 'equal_to: :a', + [ :a ], + [ :b ], + [ nil ] + + validation_test 'equal_to: [ :a, :b ]', + [ :a, :b ], + [ [ :a, :b ] ], + [ nil ] + + validation_test 'equal_to: [ [ :a, :b ] ]', + [ [ :a, :b ] ], + [ :a, :b ], + [ nil ] + + validation_test 'equal_to: nil', + [ ], + [ 'a' ], + [ nil ] + + validation_test 'equal_to: [ "a", nil ]', + [ 'a' ], + [ 'b' ], + [ nil ] + + validation_test 'equal_to: [ nil, "a" ]', + [ 'a' ], + [ 'b' ], + [ nil ] + + validation_test 'equal_to: []', + [], + [ :a ], + [ nil ] + end + + # kind_of + context "kind_of" do + validation_test 'kind_of: String', + [ 'a' ], + [ :b ], + [ nil ] + + validation_test 'kind_of: [ String, Symbol ]', + [ 'a', :b ], + [ 1 ], + [ nil ] + + validation_test 'kind_of: [ Symbol, String ]', + [ 'a', :b ], + [ 1 ], + [ nil ] + + validation_test 'kind_of: NilClass', + [ ], + [ 'a' ], + [ nil ] + + validation_test 'kind_of: [ NilClass, String ]', + [ 'a' ], + [ :a ], + [ nil ] + + validation_test 'kind_of: []', + [], + [ :a ], + [ nil ] + + validation_test 'kind_of: nil', + [], + [ :a ], + [ nil ] + end + + # regex + context "regex" do + validation_test 'regex: /abc/', + [ 'xabcy' ], + [ 'gbh', 123 ], + [ nil ] + + validation_test 'regex: [ /abc/, /z/ ]', + [ 'xabcy', 'aza' ], + [ 'gbh', 123 ], + [ nil ] + + validation_test 'regex: [ /z/, /abc/ ]', + [ 'xabcy', 'aza' ], + [ 'gbh', 123 ], + [ nil ] + + validation_test 'regex: [ [ /z/, /abc/ ], [ /n/ ] ]', + [ 'xabcy', 'aza', 'ana' ], + [ 'gbh', 123 ], + [ nil ] + + validation_test 'regex: []', + [], + [ :a ], + [ nil ] + + validation_test 'regex: nil', + [], + [ :a ], + [ nil ] + end + + # callbacks + context "callbacks" do + validation_test 'callbacks: { "a" => proc { |x| x > 10 }, "b" => proc { |x| x%2 == 0 } }', + [ 12 ], + [ 11, 4 ] + + validation_test 'callbacks: { "a" => proc { |x| x%2 == 0 }, "b" => proc { |x| x > 10 } }', + [ 12 ], + [ 11, 4 ] + + validation_test 'callbacks: { "a" => proc { |x| x.nil? } }', + [ ], + [ 'a' ], + [ nil ] + + validation_test 'callbacks: {}', + [ :a ], + [], + [ nil ] + end + + # respond_to + context "respond_to" do + validation_test 'respond_to: :split', + [ 'hi' ], + [ 1 ], + [ nil ] + + validation_test 'respond_to: "split"', + [ 'hi' ], + [ 1 ], + [ nil ] + + validation_test 'respond_to: :to_s', + [ :a ], + [], + [ nil ] + + validation_test 'respond_to: [ :split, :to_s ]', + [ 'hi' ], + [ 1 ], + [ nil ] + + validation_test 'respond_to: %w(split to_s)', + [ 'hi' ], + [ 1 ], + [ nil ] + + validation_test 'respond_to: [ :to_s, :split ]', + [ 'hi' ], + [ 1, ], + [ nil ] + + validation_test 'respond_to: []', + [ :a ], + [], + [ nil ] + + validation_test 'respond_to: nil', + [ :a ], + [], + [ nil ] + end + + context "cannot_be" do + validation_test 'cannot_be: :empty', + [ 1, [1,2], { a: 10 } ], + [ [] ], + [ nil ] + + validation_test 'cannot_be: "empty"', + [ 1, [1,2], { a: 10 } ], + [ [] ], + [ nil ] + + validation_test 'cannot_be: [ :empty, :nil ]', + [ 1, [1,2], { a: 10 } ], + [ [] ], + [ nil ] + + validation_test 'cannot_be: [ "empty", "nil" ]', + [ 1, [1,2], { a: 10 } ], + [ [] ], + [ nil ] + + validation_test 'cannot_be: [ :nil, :empty ]', + [ 1, [1,2], { a: 10 } ], + [ [] ], + [ nil ] + + validation_test 'cannot_be: [ :empty, :nil, :blahblah ]', + [ 1, [1,2], { a: 10 } ], + [ [] ], + [ nil ] + + validation_test 'cannot_be: []', + [ :a ], + [], + [ nil ] + + validation_test 'cannot_be: nil', + [ :a ], + [], + [ nil ] + + end + + context "required" do + with_property ':x, required: true' do + it "if x is not specified, retrieval fails" do + expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed + end + it "value 1 is valid" do + expect(resource.x 1).to eq 1 + expect(resource.x).to eq 1 + end + it "value nil emits a deprecation warning and does a get" do + expect { resource.x nil }.to raise_error Chef::Exceptions::DeprecatedFeatureError + Chef::Config[:treat_deprecation_warnings_as_errors] = false + resource.x 1 + expect(resource.x nil).to eq 1 + expect(resource.x).to eq 1 + end + end + + with_property ':x, [String, nil], required: true' do + it "if x is not specified, retrieval fails" do + expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed + end + it "value nil is valid" do + expect(resource.x nil).to be_nil + expect(resource.x).to be_nil + end + it "value '1' is valid" do + expect(resource.x '1').to eq '1' + expect(resource.x).to eq '1' + end + it "value 1 is invalid" do + expect { resource.x 1 }.to raise_error Chef::Exceptions::ValidationFailed + end + end + + with_property ':x, name_property: true, required: true' do + it "if x is not specified, the name property is returned" do + expect(resource.x).to eq 'blah' + end + it "value 1 is valid" do + expect(resource.x 1).to eq 1 + expect(resource.x).to eq 1 + end + it "value nil emits a deprecation warning and does a get" do + expect { resource.x nil }.to raise_error Chef::Exceptions::DeprecatedFeatureError + Chef::Config[:treat_deprecation_warnings_as_errors] = false + resource.x 1 + expect(resource.x nil).to eq 1 + expect(resource.x).to eq 1 + end + end + + with_property ':x, default: 10, required: true' do + it "if x is not specified, the default is returned" do + expect(resource.x).to eq 10 + end + it "value 1 is valid" do + expect(resource.x 1).to eq 1 + expect(resource.x).to eq 1 + end + it "value nil is invalid" do + expect { resource.x nil }.to raise_error Chef::Exceptions::DeprecatedFeatureError + Chef::Config[:treat_deprecation_warnings_as_errors] = false + resource.x 1 + expect(resource.x nil).to eq 1 + expect(resource.x).to eq 1 + end + end + end + + context "custom validators (def _pv_blarghle)" do + before do + Chef::Config[:treat_deprecation_warnings_as_errors] = false + end + + with_property ':x, blarghle: 1' do + context "and a class that implements _pv_blarghle" do + before do + resource_class.class_eval do + def _pv_blarghle(opts, key, value) + if _pv_opts_lookup(opts, key) != value + raise Chef::Exceptions::ValidationFailed, "ouch" + end + end + end + end + + it "value 1 is valid" do + expect(resource.x 1).to eq 1 + expect(resource.x).to eq 1 + end + + it "value '1' is invalid" do + Chef::Config[:treat_deprecation_warnings_as_errors] = false + expect { resource.x '1' }.to raise_error Chef::Exceptions::ValidationFailed + end + + it "value nil does a get" do + Chef::Config[:treat_deprecation_warnings_as_errors] = false + resource.x 1 + resource.x nil + expect(resource.x).to eq 1 + end + end + end + + with_property ':x, blarghle: 1' do + context "and a class that implements _pv_blarghle" do + before do + resource_class.class_eval do + def _pv_blarghle(opts, key, value) + if _pv_opts_lookup(opts, key) != value + raise Chef::Exceptions::ValidationFailed, "ouch" + end + end + end + end + + it "value 1 is valid" do + expect(resource.x 1).to eq 1 + expect(resource.x).to eq 1 + end + + it "value '1' is invalid" do + expect { resource.x '1' }.to raise_error Chef::Exceptions::ValidationFailed + end + + it "value nil does a get" do + resource.x 1 + resource.x nil + expect(resource.x).to eq 1 + end + end + end + end +end diff --git a/spec/unit/property_spec.rb b/spec/unit/property_spec.rb new file mode 100644 index 0000000000..50764aa7a2 --- /dev/null +++ b/spec/unit/property_spec.rb @@ -0,0 +1,972 @@ +require 'support/shared/integration/integration_helper' + +describe "Chef::Resource.property" do + include IntegrationSupport + + module Namer + @i = 0 + def self.next_resource_name + "chef_resource_property_spec_#{@i += 1}" + end + def self.reset_index + @current_index = 0 + end + def self.current_index + @current_index + end + def self.next_index + @current_index += 1 + end + end + + def lazy(&block) + Chef::DelayedEvaluator.new(&block) + end + + before do + Namer.reset_index + end + + def self.new_resource_name + Namer.next_resource_name + end + + let(:resource_class) do + new_resource_name = self.class.new_resource_name + Class.new(Chef::Resource) do + resource_name new_resource_name + def next_index + Namer.next_index + end + end + end + + let(:resource) do + resource_class.new("blah") + end + + def self.english_join(values) + return '<nothing>' if values.size == 0 + return values[0].inspect if values.size == 1 + "#{values[0..-2].map { |v| v.inspect }.join(", ")} and #{values[-1].inspect}" + end + + def self.with_property(*properties, &block) + tags_index = properties.find_index { |p| !p.is_a?(String)} + if tags_index + properties, tags = properties[0..tags_index-1], properties[tags_index..-1] + else + tags = [] + end + if properties.size == 1 + description = "With property #{properties.first}" + else + description = "With properties #{english_join(properties.map { |property| "#{property.inspect}" })}" + end + context description, *tags do + before do + properties.each do |property_str| + resource_class.class_eval("property #{property_str}", __FILE__, __LINE__) + end + end + instance_eval(&block) + end + end + + # Basic properties + with_property ':bare_property' do + it "can be set" do + expect(resource.bare_property 10).to eq 10 + expect(resource.bare_property).to eq 10 + end + it "emits a deprecation warning and does a get, if set to nil" do + expect(resource.bare_property 10).to eq 10 + expect { resource.bare_property nil }.to raise_error Chef::Exceptions::DeprecatedFeatureError + Chef::Config[:treat_deprecation_warnings_as_errors] = false + expect(resource.bare_property nil).to eq 10 + expect(resource.bare_property).to eq 10 + end + it "can be updated" do + expect(resource.bare_property 10).to eq 10 + expect(resource.bare_property 20).to eq 20 + expect(resource.bare_property).to eq 20 + end + it "can be set with =" do + expect(resource.bare_property 10).to eq 10 + expect(resource.bare_property).to eq 10 + end + it "can be set to nil with =" do + expect(resource.bare_property 10).to eq 10 + expect(resource.bare_property = nil).to be_nil + expect(resource.bare_property).to be_nil + end + it "can be updated with =" do + expect(resource.bare_property 10).to eq 10 + expect(resource.bare_property = 20).to eq 20 + expect(resource.bare_property).to eq 20 + end + end + + with_property ":x, Integer" do + context "and subclass" do + let(:subresource_class) do + new_resource_name = self.class.new_resource_name + Class.new(resource_class) do + resource_name new_resource_name + end + end + let(:subresource) do + subresource_class.new('blah') + end + + it "x is inherited" do + expect(subresource.x 10).to eq 10 + expect(subresource.x).to eq 10 + expect(subresource.x = 20).to eq 20 + expect(subresource.x).to eq 20 + expect(subresource_class.properties[:x]).not_to be_nil + end + + it "x's validation is inherited" do + expect { subresource.x 'ohno' }.to raise_error Chef::Exceptions::ValidationFailed + end + + context "with property :y on the subclass" do + before do + subresource_class.class_eval do + property :y + end + end + + it "x is still there" do + expect(subresource.x 10).to eq 10 + expect(subresource.x).to eq 10 + expect(subresource.x = 20).to eq 20 + expect(subresource.x).to eq 20 + expect(subresource_class.properties[:x]).not_to be_nil + end + it "y is there" do + expect(subresource.y 10).to eq 10 + expect(subresource.y).to eq 10 + expect(subresource.y = 20).to eq 20 + expect(subresource.y).to eq 20 + expect(subresource_class.properties[:y]).not_to be_nil + end + it "y is not on the superclass" do + expect { resource_class.y 10 }.to raise_error + expect(resource_class.properties[:y]).to be_nil + end + end + + context "with property :x on the subclass" do + before do + subresource_class.class_eval do + property :x + end + end + + it "x is still there" do + expect(subresource.x 10).to eq 10 + expect(subresource.x).to eq 10 + expect(subresource.x = 20).to eq 20 + expect(subresource.x).to eq 20 + expect(subresource_class.properties[:x]).not_to be_nil + expect(subresource_class.properties[:x]).not_to eq resource_class.properties[:x] + end + + it "x's validation is inherited" do + expect { subresource.x 'ohno' }.to raise_error Chef::Exceptions::ValidationFailed + end + end + + context "with property :x, default: 80 on the subclass" do + before do + subresource_class.class_eval do + property :x, default: 80 + end + end + + it "x is still there" do + expect(subresource.x 10).to eq 10 + expect(subresource.x).to eq 10 + expect(subresource.x = 20).to eq 20 + expect(subresource.x).to eq 20 + expect(subresource_class.properties[:x]).not_to be_nil + expect(subresource_class.properties[:x]).not_to eq resource_class.properties[:x] + end + + it "x defaults to 80" do + expect(subresource.x).to eq 80 + end + + it "x's validation is inherited" do + expect { subresource.x 'ohno' }.to raise_error Chef::Exceptions::ValidationFailed + end + end + + context "with property :x, String on the subclass" do + before do + subresource_class.class_eval do + property :x, String + end + end + + it "x is still there" do + expect(subresource.x "10").to eq "10" + expect(subresource.x).to eq "10" + expect(subresource.x = "20").to eq "20" + expect(subresource.x).to eq "20" + expect(subresource_class.properties[:x]).not_to be_nil + expect(subresource_class.properties[:x]).not_to eq resource_class.properties[:x] + end + + it "x's validation is overwritten" do + expect { subresource.x 10 }.to raise_error Chef::Exceptions::ValidationFailed + expect(subresource.x 'ohno').to eq 'ohno' + expect(subresource.x).to eq 'ohno' + end + + it "the superclass's validation for x is still there" do + expect { resource.x 'ohno' }.to raise_error Chef::Exceptions::ValidationFailed + expect(resource.x 10).to eq 10 + expect(resource.x).to eq 10 + end + end + end + end + + context "Chef::Resource::Property#reset_property" do + it "when a resource is newly created, reset_property(:name) sets property to nil" do + expect(resource.property_is_set?(:name)).to be_truthy + resource.reset_property(:name) + expect(resource.property_is_set?(:name)).to be_falsey + expect(resource.name).to be_nil + end + + it "when referencing an undefined property, reset_property(:x) raises an error" do + expect { resource.reset_property(:x) }.to raise_error(ArgumentError) + end + + with_property ':x' do + it "when the resource is newly created, reset_property(:x) does nothing" do + expect(resource.property_is_set?(:x)).to be_falsey + resource.reset_property(:x) + expect(resource.property_is_set?(:x)).to be_falsey + expect(resource.x).to be_nil + end + it "when x is set, reset_property resets it" do + resource.x 10 + expect(resource.property_is_set?(:x)).to be_truthy + resource.reset_property(:x) + expect(resource.property_is_set?(:x)).to be_falsey + expect(resource.x).to be_nil + end + end + + with_property ':x, Integer' do + it "when the resource is newly created, reset_property(:x) does nothing" do + expect(resource.property_is_set?(:x)).to be_falsey + resource.reset_property(:x) + expect(resource.property_is_set?(:x)).to be_falsey + expect(resource.x).to be_nil + end + it "when x is set, reset_property resets it even though `nil` is technically invalid" do + resource.x 10 + expect(resource.property_is_set?(:x)).to be_truthy + resource.reset_property(:x) + expect(resource.property_is_set?(:x)).to be_falsey + expect(resource.x).to be_nil + end + end + + with_property ':x, default: 10' do + it "when the resource is newly created, reset_property(:x) does nothing" do + expect(resource.property_is_set?(:x)).to be_falsey + resource.reset_property(:x) + expect(resource.property_is_set?(:x)).to be_falsey + expect(resource.x).to eq 10 + end + it "when x is set, reset_property resets it and it returns the default" do + resource.x 20 + resource.reset_property(:x) + expect(resource.property_is_set?(:x)).to be_falsey + expect(resource.x).to eq 10 + end + end + + with_property ':x, default: lazy { 10 }' do + it "when the resource is newly created, reset_property(:x) does nothing" do + expect(resource.property_is_set?(:x)).to be_falsey + resource.reset_property(:x) + expect(resource.property_is_set?(:x)).to be_falsey + expect(resource.x).to eq 10 + end + it "when x is set, reset_property resets it and it returns the default" do + resource.x 20 + resource.reset_property(:x) + expect(resource.property_is_set?(:x)).to be_falsey + expect(resource.x).to eq 10 + end + end + end + + context "Chef::Resource::Property#property_is_set?" do + it "when a resource is newly created, property_is_set?(:name) is true" do + expect(resource.property_is_set?(:name)).to be_truthy + end + + it "when referencing an undefined property, property_is_set?(:x) raises an error" do + expect { resource.property_is_set?(:x) }.to raise_error(ArgumentError) + end + + with_property ':x' do + it "when the resource is newly created, property_is_set?(:x) is false" do + expect(resource.property_is_set?(:x)).to be_falsey + end + it "when x is set, property_is_set?(:x) is true" do + resource.x 10 + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is set with =, property_is_set?(:x) is true" do + resource.x = 10 + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is set to a lazy value, property_is_set?(:x) is true" do + resource.x lazy { 10 } + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is retrieved, property_is_set?(:x) is false" do + resource.x + expect(resource.property_is_set?(:x)).to be_falsey + end + end + + with_property ':x, default: 10' do + it "when the resource is newly created, property_is_set?(:x) is false" do + expect(resource.property_is_set?(:x)).to be_falsey + end + it "when x is set, property_is_set?(:x) is true" do + resource.x 10 + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is set with =, property_is_set?(:x) is true" do + resource.x = 10 + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is set to a lazy value, property_is_set?(:x) is true" do + resource.x lazy { 10 } + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is retrieved, property_is_set?(:x) is false" do + resource.x + expect(resource.property_is_set?(:x)).to be_falsey + end + end + + with_property ':x, default: nil' do + it "when the resource is newly created, property_is_set?(:x) is false" do + expect(resource.property_is_set?(:x)).to be_falsey + end + it "when x is set, property_is_set?(:x) is true" do + resource.x 10 + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is set with =, property_is_set?(:x) is true" do + resource.x = 10 + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is set to a lazy value, property_is_set?(:x) is true" do + resource.x lazy { 10 } + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is retrieved, property_is_set?(:x) is false" do + resource.x + expect(resource.property_is_set?(:x)).to be_falsey + end + end + + with_property ':x, default: lazy { 10 }' do + it "when the resource is newly created, property_is_set?(:x) is false" do + expect(resource.property_is_set?(:x)).to be_falsey + end + it "when x is set, property_is_set?(:x) is true" do + resource.x 10 + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is set with =, property_is_set?(:x) is true" do + resource.x = 10 + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is retrieved, property_is_set?(:x) is false" do + resource.x + expect(resource.property_is_set?(:x)).to be_falsey + end + end + end + + context "Chef::Resource::Property#default" do + with_property ':x, default: 10' do + it "when x is set, it returns its value" do + expect(resource.x 20).to eq 20 + expect(resource.property_is_set?(:x)).to be_truthy + expect(resource.x).to eq 20 + end + it "when x is not set, it returns 10" do + expect(resource.x).to eq 10 + end + it "when x is not set, it is not included in state" do + expect(resource.state_for_resource_reporter).to eq({}) + end + it "when x is set to nil, it returns nil" do + resource.instance_eval { @x = nil } + expect(resource.x).to be_nil + end + + context "With a subclass" do + let(:subresource_class) do + new_resource_name = self.class.new_resource_name + Class.new(resource_class) do + resource_name new_resource_name + end + end + let(:subresource) { subresource_class.new('blah') } + it "The default is inherited" do + expect(subresource.x).to eq 10 + end + end + end + + with_property ':x, default: 10, identity: true' do + it "when x is not set, it is included in identity" do + expect(resource.identity).to eq(10) + end + end + + with_property ':x, default: 1, identity: true', ':y, default: 2, identity: true' do + it "when x is not set, it is still included in identity" do + resource.y 20 + expect(resource.identity).to eq(x: 1, y: 20) + end + end + + with_property ':x, default: nil' do + it "when x is not set, it returns nil" do + expect(resource.x).to be_nil + end + end + + with_property ':x' do + it "when x is not set, it returns nil" do + expect(resource.x).to be_nil + end + end + + context "hash default" do + context "(deprecations allowed)" do + before { Chef::Config[:treat_deprecation_warnings_as_errors] = false } + + with_property ':x, default: {}' do + it "when x is not set, it returns {}" do + expect(resource.x).to eq({}) + end + it "The same exact value is returned multiple times in a row" do + value = resource.x + expect(value).to eq({}) + expect(resource.x.object_id).to eq(value.object_id) + end + it "Multiple instances of x receive the exact same value" do + expect(resource.x.object_id).to eq(resource_class.new('blah2').x.object_id) + end + end + end + + it "when a property is declared with default: {}, a warning is issued" do + expect(Chef::Log).to receive(:deprecation).with( /^Property .+\.x has an array or hash default \(\{\}\)\. This means that if one resource modifies or appends to it, all other resources of the same type will also see the changes\. Either freeze the constant with \`\.freeze\` to prevent appending, or use lazy \{ \{\} \}\.$/, /property_spec\.rb/ ) + resource_class.class_eval("property :x, default: {}", __FILE__, __LINE__) + expect(resource.x).to eq({}) + end + + with_property ':x, default: lazy { {} }' do + it "when x is not set, it returns {}" do + expect(resource.x).to eq({}) + end + # it "The value is different each time it is called" do + # value = resource.x + # expect(value).to eq({}) + # expect(resource.x.object_id).not_to eq(value.object_id) + # end + it "Multiple instances of x receive different values" do + expect(resource.x.object_id).not_to eq(resource_class.new('blah2').x.object_id) + end + end + end + + context "with a class with 'blah' as both class and instance methods" do + before do + resource_class.class_eval do + def self.blah + 'class' + end + def blah + "#{name}#{next_index}" + end + end + end + + with_property ':x, default: lazy { blah }' do + it "x is run in context of the instance" do + expect(resource.x).to eq "blah1" + end + it "x is run in the context of each instance it is run in" do + expect(resource.x).to eq "blah1" + expect(resource_class.new('another').x).to eq "another2" + # expect(resource.x).to eq "blah3" + end + end + + with_property ':x, default: lazy { |x| "#{blah}#{x.blah}" }' do + it "x is run in context of the class (where it was defined) and passed the instance" do + expect(resource.x).to eq "classblah1" + end + it "x is passed the value of each instance it is run in" do + expect(resource.x).to eq "classblah1" + expect(resource_class.new('another').x).to eq "classanother2" + # expect(resource.x).to eq "classblah3" + end + end + end + + context "validation of defaults" do + with_property ':x, String, default: 10' do + it "when the resource is created, no error is raised" do + resource + end + it "when x is set, no error is raised" do + expect(resource.x 'hi').to eq 'hi' + expect(resource.x).to eq 'hi' + end + it "when x is retrieved, no validation error is raised" do + expect(resource.x).to eq 10 + end + # it "when x is retrieved, a validation error is raised" do + # expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed + # end + end + + with_property ":x, String, default: lazy { Namer.next_index }" do + it "when the resource is created, no error is raised" do + resource + end + it "when x is set, no error is raised" do + expect(resource.x 'hi').to eq 'hi' + expect(resource.x).to eq 'hi' + end + it "when x is retrieved, no validation error is raised" do + expect(resource.x).to eq 1 + expect(Namer.current_index).to eq 1 + end + # it "when x is retrieved, a validation error is raised" do + # expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed + # expect(Namer.current_index).to eq 1 + # end + end + + with_property ":x, default: lazy { Namer.next_index.to_s }, is: proc { |v| Namer.next_index; true }" do + it "validation is not run at all on the default value" do + expect(resource.x).to eq '1' + expect(Namer.current_index).to eq 1 + end + # it "validation is run each time" do + # expect(resource.x).to eq '1' + # expect(Namer.current_index).to eq 2 + # expect(resource.x).to eq '1' + # expect(Namer.current_index).to eq 2 + # end + end + + with_property ":x, default: lazy { Namer.next_index.to_s.freeze }, is: proc { |v| Namer.next_index; true }" do + it "validation is not run at all on the default value" do + expect(resource.x).to eq '1' + expect(Namer.current_index).to eq 1 + end + # it "validation is only run the first time" do + # expect(resource.x).to eq '1' + # expect(Namer.current_index).to eq 2 + # expect(resource.x).to eq '1' + # expect(Namer.current_index).to eq 2 + # end + end + end + + context "coercion of defaults" do + # Frozen default, non-frozen coerce + with_property ':x, coerce: proc { |v| "#{v}#{next_index}" }, default: 10' do + it "when the resource is created, the proc is not yet run" do + resource + expect(Namer.current_index).to eq 0 + end + it "when x is set, coercion is run" do + expect(resource.x 'hi').to eq 'hi1' + expect(resource.x).to eq 'hi1' + expect(Namer.current_index).to eq 1 + end + it "when x is retrieved, coercion is run exactly once" do + expect(resource.x).to eq '101' + expect(resource.x).to eq '101' + expect(Namer.current_index).to eq 1 + end + end + + # Frozen default, frozen coerce + with_property ':x, coerce: proc { |v| "#{v}#{next_index}".freeze }, default: 10' do + it "when the resource is created, the proc is not yet run" do + resource + expect(Namer.current_index).to eq 0 + end + it "when x is set, coercion is run" do + expect(resource.x 'hi').to eq 'hi1' + expect(resource.x).to eq 'hi1' + expect(Namer.current_index).to eq 1 + end + it "when x is retrieved, coercion is run each time" do + expect(resource.x).to eq '101' + expect(resource.x).to eq '102' + expect(Namer.current_index).to eq 2 + end + end + + # Frozen lazy default, non-frozen coerce + with_property ':x, coerce: proc { |v| "#{v}#{next_index}" }, default: lazy { 10 }' do + it "when the resource is created, the proc is not yet run" do + resource + expect(Namer.current_index).to eq 0 + end + it "when x is set, coercion is run" do + expect(resource.x 'hi').to eq 'hi1' + expect(resource.x).to eq 'hi1' + expect(Namer.current_index).to eq 1 + end + it "when x is retrieved, coercion is run exactly once" do + expect(resource.x).to eq '101' + expect(resource.x).to eq '101' + expect(Namer.current_index).to eq 1 + end + end + + # Non-frozen lazy default, frozen coerce + with_property ':x, coerce: proc { |v| "#{v}#{next_index}".freeze }, default: lazy { "10" }' do + it "when the resource is created, the proc is not yet run" do + resource + expect(Namer.current_index).to eq 0 + end + it "when x is set, coercion is run" do + expect(resource.x 'hi').to eq 'hi1' + expect(resource.x).to eq 'hi1' + expect(Namer.current_index).to eq 1 + end + it "when x is retrieved, coercion is run each time" do + expect(resource.x).to eq '101' + expect(resource.x).to eq '102' + expect(Namer.current_index).to eq 2 + end + end + + with_property ':x, proc { |v| Namer.next_index; true }, coerce: proc { |v| "#{v}#{next_index}" }, default: lazy { 10 }' do + it "coercion is only run the first time x is retrieved, and validation is not run" do + expect(Namer.current_index).to eq 0 + expect(resource.x).to eq '101' + expect(Namer.current_index).to eq 1 + expect(resource.x).to eq '101' + expect(Namer.current_index).to eq 1 + end + end + + context "validation and coercion of defaults" do + with_property ':x, String, coerce: proc { |v| "#{v}#{next_index}" }, default: 10' do + it "when x is retrieved, it is coerced before validating and passes" do + expect(resource.x).to eq '101' + end + end + with_property ':x, Integer, coerce: proc { |v| "#{v}#{next_index}" }, default: 10' do + it "when x is retrieved, it is coerced and not validated" do + expect(resource.x).to eq '101' + end + # it "when x is retrieved, it is coerced before validating and fails" do + # expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed + # end + end + with_property ':x, String, coerce: proc { |v| "#{v}#{next_index}" }, default: lazy { 10 }' do + it "when x is retrieved, it is coerced before validating and passes" do + expect(resource.x).to eq '101' + end + end + with_property ':x, Integer, coerce: proc { |v| "#{v}#{next_index}" }, default: lazy { 10 }' do + it "when x is retrieved, it is coerced and not validated" do + expect(resource.x).to eq '101' + end + # it "when x is retrieved, it is coerced before validating and fails" do + # expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed + # end + end + with_property ':x, proc { |v| Namer.next_index; true }, coerce: proc { |v| "#{v}#{next_index}" }, default: lazy { 10 }' do + it "coercion is only run the first time x is retrieved, and validation is not run" do + expect(Namer.current_index).to eq 0 + expect(resource.x).to eq '101' + expect(Namer.current_index).to eq 1 + expect(resource.x).to eq '101' + expect(Namer.current_index).to eq 1 + end + end + end + end + end + + context "Chef::Resource#lazy" do + with_property ':x' do + it "setting x to a lazy value does not run it immediately" do + resource.x lazy { Namer.next_index } + expect(Namer.current_index).to eq 0 + end + it "you can set x to a lazy value in the instance" do + resource.instance_eval do + x lazy { Namer.next_index } + end + expect(resource.x).to eq 1 + expect(Namer.current_index).to eq 1 + end + it "retrieving a lazy value pops it open" do + resource.x lazy { Namer.next_index } + expect(resource.x).to eq 1 + expect(Namer.current_index).to eq 1 + end + it "retrieving a lazy value twice evaluates it twice" do + resource.x lazy { Namer.next_index } + expect(resource.x).to eq 1 + expect(resource.x).to eq 2 + expect(Namer.current_index).to eq 2 + end + it "setting the same lazy value on two different instances runs it on each instancee" do + resource2 = resource_class.new("blah2") + l = lazy { Namer.next_index } + resource.x l + resource2.x l + expect(resource2.x).to eq 1 + expect(resource.x).to eq 2 + expect(resource2.x).to eq 3 + end + + context "when the class has a class and instance method named blah" do + before do + resource_class.class_eval do + def self.blah + "class" + end + def blah + "#{name}#{Namer.next_index}" + end + end + end + def blah + "example" + end + # it "retrieving lazy { blah } gets the instance variable" do + # resource.x lazy { blah } + # expect(resource.x).to eq "blah1" + # end + # it "retrieving lazy { blah } from two different instances gets two different instance variables" do + # resource2 = resource_class.new("another") + # l = lazy { blah } + # resource2.x l + # resource.x l + # expect(resource2.x).to eq "another1" + # expect(resource.x).to eq "blah2" + # expect(resource2.x).to eq "another3" + # end + it 'retrieving lazy { |x| "#{blah}#{x.blah}" } gets the example and instance variables' do + resource.x lazy { |x| "#{blah}#{x.blah}" } + expect(resource.x).to eq "exampleblah1" + end + it 'retrieving lazy { |x| "#{blah}#{x.blah}" } from two different instances gets two different instance variables' do + resource2 = resource_class.new("another") + l = lazy { |x| "#{blah}#{x.blah}" } + resource2.x l + resource.x l + expect(resource2.x).to eq "exampleanother1" + expect(resource.x).to eq "exampleblah2" + expect(resource2.x).to eq "exampleanother3" + end + end + end + + with_property ':x, coerce: proc { |v| "#{v}#{Namer.next_index}" }' do + it "lazy values are not coerced on set" do + resource.x lazy { Namer.next_index } + expect(Namer.current_index).to eq 0 + end + it "lazy values are coerced on get" do + resource.x lazy { Namer.next_index } + expect(resource.x).to eq "12" + expect(Namer.current_index).to eq 2 + end + it "lazy values are coerced on each access" do + resource.x lazy { Namer.next_index } + expect(resource.x).to eq "12" + expect(Namer.current_index).to eq 2 + expect(resource.x).to eq "34" + expect(Namer.current_index).to eq 4 + end + end + + with_property ':x, String' do + it "lazy values are not validated on set" do + resource.x lazy { Namer.next_index } + expect(Namer.current_index).to eq 0 + end + it "lazy values are validated on get" do + resource.x lazy { Namer.next_index } + expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed + expect(Namer.current_index).to eq 1 + end + end + + with_property ':x, is: proc { |v| Namer.next_index; true }' do + it "lazy values are validated on each access" do + resource.x lazy { Namer.next_index } + expect(resource.x).to eq 1 + expect(Namer.current_index).to eq 2 + expect(resource.x).to eq 3 + expect(Namer.current_index).to eq 4 + end + end + + with_property ':x, Integer, coerce: proc { |v| "#{v}#{Namer.next_index}" }' do + it "lazy values are not validated or coerced on set" do + resource.x lazy { Namer.next_index } + expect(Namer.current_index).to eq 0 + end + it "lazy values are coerced before being validated, which fails" do + resource.x lazy { Namer.next_index } + expect(Namer.current_index).to eq 0 + expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed + expect(Namer.current_index).to eq 2 + end + end + + with_property ':x, coerce: proc { |v| "#{v}#{Namer.next_index}" }, is: proc { |v| Namer.next_index; true }' do + it "lazy values are coerced and validated exactly once" do + resource.x lazy { Namer.next_index } + expect(resource.x).to eq "12" + expect(Namer.current_index).to eq 3 + expect(resource.x).to eq "45" + expect(Namer.current_index).to eq 6 + end + end + + with_property ':x, String, coerce: proc { |v| "#{v}#{Namer.next_index}" }' do + it "lazy values are coerced before being validated, which succeeds" do + resource.x lazy { Namer.next_index } + expect(resource.x).to eq "12" + expect(Namer.current_index).to eq 2 + end + end + end + + context "Chef::Resource::Property#coerce" do + with_property ':x, coerce: proc { |v| "#{v}#{Namer.next_index}" }' do + it "coercion runs on set" do + expect(resource.x 10).to eq "101" + expect(Namer.current_index).to eq 1 + end + it "coercion sets the value (and coercion does not run on get)" do + expect(resource.x 10).to eq "101" + expect(resource.x).to eq "101" + expect(Namer.current_index).to eq 1 + end + it "coercion runs each time set happens" do + expect(resource.x 10).to eq "101" + expect(Namer.current_index).to eq 1 + expect(resource.x 10).to eq "102" + expect(Namer.current_index).to eq 2 + end + end + with_property ':x, coerce: proc { |x| Namer.next_index; raise "hi" if x == 10; x }, is: proc { |x| Namer.next_index; x != 10 }' do + it "failed coercion fails to set the value" do + resource.x 20 + expect(resource.x).to eq 20 + expect(Namer.current_index).to eq 2 + expect { resource.x 10 }.to raise_error 'hi' + expect(resource.x).to eq 20 + expect(Namer.current_index).to eq 3 + end + it "validation does not run if coercion fails" do + expect { resource.x 10 }.to raise_error 'hi' + expect(Namer.current_index).to eq 1 + end + end + end + + context "Chef::Resource::Property validation" do + with_property ':x, is: proc { |v| Namer.next_index; v.is_a?(Integer) }' do + it "validation runs on set" do + expect(resource.x 10).to eq 10 + expect(Namer.current_index).to eq 1 + end + it "validation sets the value (and validation does not run on get)" do + expect(resource.x 10).to eq 10 + expect(resource.x).to eq 10 + expect(Namer.current_index).to eq 1 + end + it "validation runs each time set happens" do + expect(resource.x 10).to eq 10 + expect(Namer.current_index).to eq 1 + expect(resource.x 10).to eq 10 + expect(Namer.current_index).to eq 2 + end + it "failed validation fails to set the value" do + expect(resource.x 10).to eq 10 + expect(Namer.current_index).to eq 1 + expect { resource.x 'blah' }.to raise_error Chef::Exceptions::ValidationFailed + expect(resource.x).to eq 10 + expect(Namer.current_index).to eq 2 + end + end + end + + [ 'name_attribute', 'name_property' ].each do |name| + context "Chef::Resource::Property##{name}" do + with_property ":x, #{name}: true" do + it "defaults x to resource.name" do + expect(resource.x).to eq 'blah' + end + it "does not pick up resource.name if set" do + expect(resource.x 10).to eq 10 + expect(resource.x).to eq 10 + end + it "binds to the latest resource.name when run" do + resource.name 'foo' + expect(resource.x).to eq 'foo' + end + it "caches resource.name" do + expect(resource.x).to eq 'blah' + resource.name 'foo' + expect(resource.x).to eq 'blah' + end + end + with_property ":x, default: 10, #{name}: true" do + it "chooses default over #{name}" do + expect(resource.x).to eq 10 + end + end + with_property ":x, #{name}: true, default: 10" do + it "chooses default over #{name}" do + expect(resource.x).to eq 10 + end + end + with_property ":x, #{name}: true, required: true" do + it "defaults x to resource.name" do + expect(resource.x).to eq 'blah' + end + end + end + end +end diff --git a/spec/unit/provider/deploy/revision_spec.rb b/spec/unit/provider/deploy/revision_spec.rb index 4ca64e3445..caa60878e1 100644 --- a/spec/unit/provider/deploy/revision_spec.rb +++ b/spec/unit/provider/deploy/revision_spec.rb @@ -21,7 +21,7 @@ require 'spec_helper' describe Chef::Provider::Deploy::Revision do before do - allow(Chef::Platform).to receive(:windows?) { false } + allow(ChefConfig).to receive(:windows?) { false } @temp_dir = Dir.mktmpdir Chef::Config[:file_cache_path] = @temp_dir @resource = Chef::Resource::Deploy.new("/my/deploy/dir") diff --git a/spec/unit/provider/deploy_spec.rb b/spec/unit/provider/deploy_spec.rb index c95a9b3d57..f6bb78823f 100644 --- a/spec/unit/provider/deploy_spec.rb +++ b/spec/unit/provider/deploy_spec.rb @@ -21,7 +21,7 @@ require 'spec_helper' describe Chef::Provider::Deploy do before do - allow(Chef::Platform).to receive(:windows?) { false } + allow(ChefConfig).to receive(:windows?) { false } @release_time = Time.utc( 2004, 8, 15, 16, 23, 42) allow(Time).to receive(:now).and_return(@release_time) @expected_release_dir = "/my/deploy/dir/releases/20040815162342" @@ -622,7 +622,7 @@ describe Chef::Provider::Deploy do gems = @provider.send(:gem_packages) - expect(gems.map { |g| g.action }).to eq([[:install]]) + expect(gems.map { |g| g.action }).to eq([%i{install}]) expect(gems.map { |g| g.name }).to eq(%w{eventmachine}) expect(gems.map { |g| g.version }).to eq(%w{0.12.9}) end diff --git a/spec/unit/provider/directory_spec.rb b/spec/unit/provider/directory_spec.rb index 13c57bfe56..4fad8c8906 100644 --- a/spec/unit/provider/directory_spec.rb +++ b/spec/unit/provider/directory_spec.rb @@ -16,173 +16,272 @@ # limitations under the License. # -require 'ostruct' - require 'spec_helper' require 'tmpdir' describe Chef::Provider::Directory do - before(:each) do - @new_resource = Chef::Resource::Directory.new(Dir.tmpdir) - if !windows? - @new_resource.owner(500) - @new_resource.group(500) - @new_resource.mode(0644) + let(:tmp_dir) { Dir.mktmpdir } + let(:new_resource) { Chef::Resource::Directory.new(tmp_dir) } + let(:node) { Chef::Node.new } + let(:events) { Chef::EventDispatch::Dispatcher.new } + let(:run_context) { Chef::RunContext.new(node, {}, events) } + let(:directory) { Chef::Provider::Directory.new(new_resource, run_context) } + + describe "#load_current_resource" do + describe "scanning file security metadata" + describe "on unix", unix_only: true do + describe "when the directory exists" do + let(:dir_stat) { File::Stat.new(tmp_dir) } + let(:expected_uid) { dir_stat.uid } + let(:expected_gid) { dir_stat.gid } + let(:expected_mode) { "0%o" % ( dir_stat.mode & 007777 ) } + let(:expected_pwnam) { Etc.getpwuid(expected_uid).name } + let(:expected_grnam) { Etc.getgrgid(expected_gid).name } + + it "describes the access mode as a String of octal integers" do + directory.load_current_resource + expect(directory.current_resource.mode).to eq(expected_mode) + end + + it "when the new_resource.owner is numeric, describes the owner as a numeric uid" do + new_resource.owner(500) + directory.load_current_resource + expect(directory.current_resource.owner).to eql(expected_uid) + end + + it "when the new_resource.group is numeric, describes the group as a numeric gid" do + new_resource.group(500) + directory.load_current_resource + expect(directory.current_resource.group).to eql(expected_gid) + end + + it "when the new_resource.owner is a string, describes the owner as a string" do + new_resource.owner("foo") + directory.load_current_resource + expect(directory.current_resource.owner).to eql(expected_pwnam) + end + + it "when the new_resource.group is a string, describes the group as a string" do + new_resource.group("bar") + directory.load_current_resource + expect(directory.current_resource.group).to eql(expected_grnam) + end + end end - @node = Chef::Node.new - @events = Chef::EventDispatch::Dispatcher.new - @run_context = Chef::RunContext.new(@node, {}, @events) - @directory = Chef::Provider::Directory.new(@new_resource, @run_context) - end + describe "on windows", windows_only: true do + describe "when the directory exists" do + it "the mode is always nil" do + directory.load_current_resource + expect(directory.current_resource.mode).to be nil + end + + it "the owner is always nil" do + directory.load_current_resource + expect(directory.current_resource.owner).to be nil + end + it "the group is always nil" do + directory.load_current_resource + expect(directory.current_resource.group).to be nil + end - describe "scanning file security metadata on windows" do - before do + it "rights are always nil (incorrectly)" do + directory.load_current_resource + expect(directory.current_resource.rights).to be nil + end + + it "inherits is always nil (incorrectly)" do + directory.load_current_resource + expect(directory.current_resource.inherits).to be nil + end + end end - it "describes the directory's access rights" do - skip + describe "when the directory does not exist" do + before do + FileUtils.rmdir tmp_dir + end + + it "sets the mode, group and owner to nil" do + directory.load_current_resource + expect(directory.current_resource.mode).to eq(nil) + expect(directory.current_resource.group).to eq(nil) + expect(directory.current_resource.owner).to eq(nil) + end end + end - describe "scanning file security metadata on unix" do - before do - allow(Chef::Platform).to receive(:windows?).and_return(false) - end - let(:mock_stat) do - cstats = double("stats") - allow(cstats).to receive(:uid).and_return(500) - allow(cstats).to receive(:gid).and_return(500) - allow(cstats).to receive(:mode).and_return(0755) - cstats + describe "#define_resource_requirements" do + describe "on unix", unix_only: true do + it "raises an exception if the user does not exist" do + new_resource.owner("arglebargle_iv") + expect(Etc).to receive(:getpwnam).with("arglebargle_iv").and_raise(ArgumentError) + directory.action = :create + directory.load_current_resource + expect(directory.access_controls).to receive(:define_resource_requirements).and_call_original + directory.define_resource_requirements + expect { directory.process_resource_requirements }.to raise_error(ArgumentError) + end + + it "raises an exception if the group does not exist" do + new_resource.group("arglebargle_iv") + expect(Etc).to receive(:getgrnam).with("arglebargle_iv").and_raise(ArgumentError) + directory.action = :create + directory.load_current_resource + expect(directory.access_controls).to receive(:define_resource_requirements).and_call_original + directory.define_resource_requirements + expect { directory.process_resource_requirements }.to raise_error(ArgumentError) + end end + end + + describe "#run_action(:create)" do + describe "when the directory exists" do + it "does not create the directory" do + expect(Dir).not_to receive(:mkdir).with(new_resource.path) + directory.run_action(:create) + end - it "describes the access mode as a String of octal integers" do - allow(File).to receive(:exists?).and_return(true) - expect(File).to receive(:stat).and_return(mock_stat) - @directory.load_current_resource - expect(@directory.current_resource.mode).to eq("0755") + it "should not set the resource as updated" do + directory.run_action(:create) + expect(new_resource).not_to be_updated + end end - context "when user and group are specified with UID/GID" do - it "describes the current owner and group as UID and GID" do - allow(File).to receive(:exists?).and_return(true) - expect(File).to receive(:stat).and_return(mock_stat) - @directory.load_current_resource - expect(@directory.current_resource.path).to eql(@new_resource.path) - expect(@directory.current_resource.owner).to eql(500) - expect(@directory.current_resource.group).to eql(500) + describe "when the directory does not exist" do + before do + FileUtils.rmdir tmp_dir + end + + it "creates the directory" do + directory.run_action(:create) + expect(File.exist?(tmp_dir)).to be true + end + + it "sets the new resource as updated" do + directory.run_action(:create) + expect(new_resource).to be_updated end end - context "when user/group are specified with user/group names" do + describe "when the parent directory does not exist" do + before do + new_resource.path "#{tmp_dir}/foobar" + FileUtils.rmdir tmp_dir + end + + it "raises an exception when recursive is false" do + new_resource.recursive false + expect { directory.run_action(:create) }.to raise_error(Chef::Exceptions::EnclosingDirectoryDoesNotExist) + end + + it "creates the directories when recursive is true" do + new_resource.recursive true + directory.run_action(:create) + expect(new_resource).to be_updated + expect(File.exist?("#{tmp_dir}/foobar")).to be true + end + + it "raises an exception when the parent directory is a file and recursive is true" do + FileUtils.touch tmp_dir + new_resource.recursive true + expect { directory.run_action(:create) }.to raise_error + end + + it "raises the right exception when the parent directory is a file and recursive is true" do + pending "this seems to return the wrong error" # FIXME + FileUtils.touch tmp_dir + new_resource.recursive true + expect { directory.run_action(:create) }.to raise_error(Chef::Exceptions::EnclosingDirectoryDoesNotExist) + end end - end - # Unix only for now. While file security attribute reporting for windows is - # disabled, unix and windows differ in the number of exists? calls that are - # made by the provider. - it "should create a new directory on create, setting updated to true", :unix_only do - @new_resource.path "/tmp/foo" + describe "on OS X" do + before do + allow(node).to receive(:[]).with("platform").and_return('mac_os_x') + new_resource.path "/usr/bin/chef_test" + new_resource.recursive false + allow_any_instance_of(Chef::Provider::File).to receive(:do_selinux) + end - expect(File).to receive(:exists?).at_least(:once).and_return(false) - expect(File).to receive(:directory?).with("/tmp").and_return(true) - expect(Dir).to receive(:mkdir).with(@new_resource.path).once.and_return(true) + it "os x 10.10 can write to sip locations" do + allow(node).to receive(:[]).with("platform_version").and_return('10.10') + allow(Dir).to receive(:mkdir).and_return([true], []) + allow(::File).to receive(:directory?).and_return(true) + allow(Chef::FileAccessControl).to receive(:writable?).and_return(true) + directory.run_action(:create) + expect(new_resource).to be_updated + end - expect(@directory).to receive(:do_acl_changes) - allow(@directory).to receive(:do_selinux) - @directory.run_action(:create) - expect(@directory.new_resource).to be_updated - end + it "os x 10.11 cannot write to sip locations" do + allow(node).to receive(:[]).with("platform_version").and_return('10.11') + allow(::File).to receive(:directory?).and_return(true) + allow(Chef::FileAccessControl).to receive(:writable?).and_return(false) + expect {directory.run_action(:create) }.to raise_error(Chef::Exceptions::InsufficientPermissions) + end - it "should raise an exception if the parent directory does not exist and recursive is false" do - @new_resource.path "/tmp/some/dir" - @new_resource.recursive false - expect { @directory.run_action(:create) }.to raise_error(Chef::Exceptions::EnclosingDirectoryDoesNotExist) + it "os x 10.11 can write to sip exlcusions" do + new_resource.path "/usr/local/chef_test" + allow(node).to receive(:[]).with("platform_version").and_return('10.11') + allow(::File).to receive(:directory?).and_return(true) + allow(Dir).to receive(:mkdir).and_return([true], []) + allow(Chef::FileAccessControl).to receive(:writable?).and_return(false) + directory.run_action(:create) + expect(new_resource).to be_updated + end + end end - # Unix only for now. While file security attribute reporting for windows is - # disabled, unix and windows differ in the number of exists? calls that are - # made by the provider. - it "should create a new directory when parent directory does not exist if recursive is true and permissions are correct", :unix_only do - @new_resource.path "/path/to/dir" - @new_resource.recursive true - expect(File).to receive(:exists?).with(@new_resource.path).ordered.and_return(false) - - expect(File).to receive(:exists?).with('/path/to').ordered.and_return(false) - expect(File).to receive(:exists?).with('/path').ordered.and_return(true) - expect(Chef::FileAccessControl).to receive(:writable?).with('/path').ordered.and_return(true) - expect(File).to receive(:exists?).with(@new_resource.path).ordered.and_return(false) - - expect(FileUtils).to receive(:mkdir_p).with(@new_resource.path).and_return(true) - expect(@directory).to receive(:do_acl_changes) - allow(@directory).to receive(:do_selinux) - @directory.run_action(:create) - expect(@new_resource).to be_updated - end + describe "#run_action(:create)" do + describe "when the directory exists" do + it "deletes the directory" do + directory.run_action(:delete) + expect(File.exist?(tmp_dir)).to be false + end + it "sets the new resource as updated" do + directory.run_action(:delete) + expect(new_resource).to be_updated + end + end - it "should raise an error when creating a directory when parent directory is a file" do - expect(File).to receive(:directory?).and_return(false) - expect(Dir).not_to receive(:mkdir).with(@new_resource.path) - expect { @directory.run_action(:create) }.to raise_error(Chef::Exceptions::EnclosingDirectoryDoesNotExist) - expect(@directory.new_resource).not_to be_updated - end + describe "when the directory does not exist" do + before do + FileUtils.rmdir tmp_dir + end - # Unix only for now. While file security attribute reporting for windows is - # disabled, unix and windows differ in the number of exists? calls that are - # made by the provider. - it "should not create the directory if it already exists", :unix_only do - stub_file_cstats - @new_resource.path "/tmp/foo" - expect(File).to receive(:directory?).at_least(:once).and_return(true) - expect(Chef::FileAccessControl).to receive(:writable?).with("/tmp").and_return(true) - expect(File).to receive(:exists?).at_least(:once).and_return(true) - expect(Dir).not_to receive(:mkdir).with(@new_resource.path) - expect(@directory).to receive(:do_acl_changes) - @directory.run_action(:create) - end + it "does not delete the directory" do + expect(Dir).not_to receive(:delete).with(new_resource.path) + directory.run_action(:delete) + end - it "should delete the directory if it exists, and is writable with action_delete" do - expect(File).to receive(:directory?).and_return(true) - expect(Chef::FileAccessControl).to receive(:writable?).once.and_return(true) - expect(Dir).to receive(:delete).with(@new_resource.path).once.and_return(true) - @directory.run_action(:delete) - end + it "sets the new resource as updated" do + directory.run_action(:delete) + expect(new_resource).not_to be_updated + end + end - it "should raise an exception if it cannot delete the directory due to bad permissions" do - allow(File).to receive(:exists?).and_return(true) - allow(Chef::FileAccessControl).to receive(:writable?).and_return(false) - expect { @directory.run_action(:delete) }.to raise_error(RuntimeError) - end + describe "when the directory is not writable" do + before do + allow(Chef::FileAccessControl).to receive(:writable?).and_return(false) + end - it "should take no action when deleting a target directory that does not exist" do - @new_resource.path "/an/invalid/path" - allow(File).to receive(:exists?).and_return(false) - expect(Dir).not_to receive(:delete).with(@new_resource.path) - @directory.run_action(:delete) - expect(@directory.new_resource).not_to be_updated - end + it "cannot delete it and raises an exception" do + expect { directory.run_action(:delete) }.to raise_error(RuntimeError) + end + end - it "should raise an exception when deleting a directory when target directory is a file" do - stub_file_cstats - @new_resource.path "/an/invalid/path" - allow(File).to receive(:exists?).and_return(true) - expect(File).to receive(:directory?).and_return(false) - expect(Dir).not_to receive(:delete).with(@new_resource.path) - expect { @directory.run_action(:delete) }.to raise_error(RuntimeError) - expect(@directory.new_resource).not_to be_updated - end + describe "when the target directory is a file" do + before do + FileUtils.rmdir tmp_dir + FileUtils.touch tmp_dir + end - def stub_file_cstats - cstats = double("stats") - allow(cstats).to receive(:uid).and_return(500) - allow(cstats).to receive(:gid).and_return(500) - allow(cstats).to receive(:mode).and_return(0755) - # File.stat is called in: - # - Chef::Provider::File.load_current_resource_attrs - # - Chef::ScanAccessControl via Chef::Provider::File.setup_acl - allow(File).to receive(:stat).and_return(cstats) + it "cannot delete it and raises an exception" do + expect { directory.run_action(:delete) }.to raise_error(RuntimeError) + end + end end end diff --git a/spec/unit/provider/dsc_resource_spec.rb b/spec/unit/provider/dsc_resource_spec.rb new file mode 100644 index 0000000000..65c1c019f0 --- /dev/null +++ b/spec/unit/provider/dsc_resource_spec.rb @@ -0,0 +1,84 @@ +# +# Author:: Jay Mundrawala (<jdm@chef.io>) +# +# 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' +require 'spec_helper' + +describe Chef::Provider::DscResource do + let (:events) { Chef::EventDispatch::Dispatcher.new } + let (:run_context) { Chef::RunContext.new(node, {}, events) } + let (:resource) { Chef::Resource::DscResource.new("dscresource", run_context) } + let (:provider) do + Chef::Provider::DscResource.new(resource, run_context) + end + + context 'when Powershell does not support Invoke-DscResource' do + let (:node) { + node = Chef::Node.new + node.automatic[:languages][:powershell][:version] = '4.0' + node + } + + it 'raises a ProviderNotFound exception' do + expect(provider).not_to receive(:meta_configuration) + expect{provider.run_action(:run)}.to raise_error( + Chef::Exceptions::ProviderNotFound, /5\.0\.10018\.0/) + end + end + + context 'when Powershell supports Invoke-DscResource' do + let (:node) { + node = Chef::Node.new + node.automatic[:languages][:powershell][:version] = '5.0.10018.0' + node + } + + context 'when RefreshMode is not set to Disabled' do + let (:meta_configuration) { {'RefreshMode' => 'AnythingElse'}} + + it 'raises an exception' do + expect(provider).to receive(:meta_configuration).and_return( + meta_configuration) + expect { provider.run_action(:run) }.to raise_error( + Chef::Exceptions::ProviderNotFound, /Disabled/) + end + end + + context 'when RefreshMode is set to Disabled' do + let (:meta_configuration) { {'RefreshMode' => 'Disabled'}} + + it 'does not update the resource if it is up to date' do + expect(provider).to receive(:meta_configuration).and_return( + meta_configuration) + expect(provider).to receive(:test_resource).and_return(true) + provider.run_action(:run) + expect(resource).not_to be_updated + end + + it 'converges the resource if it is not up to date' do + expect(provider).to receive(:meta_configuration).and_return( + meta_configuration) + expect(provider).to receive(:test_resource).and_return(false) + expect(provider).to receive(:set_resource) + provider.run_action(:run) + expect(resource).to be_updated + end + end + end +end diff --git a/spec/unit/provider/dsc_script_spec.rb b/spec/unit/provider/dsc_script_spec.rb index d4b2eb3b22..76589e71c1 100644 --- a/spec/unit/provider/dsc_script_spec.rb +++ b/spec/unit/provider/dsc_script_spec.rb @@ -158,14 +158,14 @@ describe Chef::Provider::DscScript do expect { provider.run_action(:run) - }.to raise_error(Chef::Exceptions::NoProviderAvailable) + }.to raise_error(Chef::Exceptions::ProviderNotFound) end end it 'raises an exception if Powershell is not present' do expect { provider.run_action(:run) - }.to raise_error(Chef::Exceptions::NoProviderAvailable) + }.to raise_error(Chef::Exceptions::ProviderNotFound) end end diff --git a/spec/unit/provider/execute_spec.rb b/spec/unit/provider/execute_spec.rb index 51305b6225..1274203ce3 100644 --- a/spec/unit/provider/execute_spec.rb +++ b/spec/unit/provider/execute_spec.rb @@ -39,7 +39,7 @@ describe Chef::Provider::Execute do let(:new_resource) { Chef::Resource::Execute.new("foo_resource", run_context) } before do - allow(Chef::Platform).to receive(:windows?) { false } + allow(ChefConfig).to receive(:windows?) { false } @original_log_level = Chef::Log.level Chef::Log.level = :info allow(STDOUT).to receive(:tty?).and_return(true) diff --git a/spec/unit/provider/ifconfig/debian_spec.rb b/spec/unit/provider/ifconfig/debian_spec.rb index 351e734040..0c02ae9cd4 100644 --- a/spec/unit/provider/ifconfig/debian_spec.rb +++ b/spec/unit/provider/ifconfig/debian_spec.rb @@ -144,11 +144,6 @@ EOF expect(IO.read(tempfile.path)).to eq(expected_string) end - it "should not mark the resource as updated" do - provider.run_action(:add) - pending "superclass ifconfig provider is not idempotent" - expect(new_resource.updated_by_last_action?).to be_falsey - end end context "when the /etc/network/interfaces file does not have the source line" do @@ -280,11 +275,6 @@ another line expect(IO.read(tempfile.path)).to eq(expected_string) end - it "should not mark the resource as updated" do - provider.run_action(:add) - pending "superclass ifconfig provider is not idempotent" - expect(new_resource.updated_by_last_action?).to be_falsey - end end context "when the /etc/network/interfaces file does not have the source line" do diff --git a/spec/unit/provider/ifconfig_spec.rb b/spec/unit/provider/ifconfig_spec.rb index d290ab7066..4940f19a45 100644 --- a/spec/unit/provider/ifconfig_spec.rb +++ b/spec/unit/provider/ifconfig_spec.rb @@ -46,7 +46,7 @@ describe Chef::Provider::Ifconfig do allow(@provider).to receive(:shell_out).and_return(@status) @provider.load_current_resource end - it "should track state of ifconfig failure." do + it "should track state of ifconfig failure" do expect(@provider.instance_variable_get("@status").exitstatus).not_to eq(0) end it "should thrown an exception when ifconfig fails" do @@ -68,6 +68,16 @@ describe Chef::Provider::Ifconfig do expect(@new_resource).to be_updated end + it "should set the address to target if specified" do + allow(@provider).to receive(:load_current_resource) + @new_resource.target "172.16.32.2" + command = "ifconfig eth0 172.16.32.2 netmask 255.255.254.0 metric 1 mtu 1500" + expect(@provider).to receive(:run_command).with(:command => command) + + @provider.run_action(:add) + expect(@new_resource).to be_updated + end + it "should not add an interface if it already exists" do allow(@provider).to receive(:load_current_resource) expect(@provider).not_to receive(:run_command) @@ -85,7 +95,7 @@ describe Chef::Provider::Ifconfig do describe Chef::Provider::Ifconfig, "action_enable" do - it "should enable interface if does not exist" do + it "should enable interface if it does not exist" do allow(@provider).to receive(:load_current_resource) @current_resource.inet_addr nil command = "ifconfig eth0 10.0.0.1 netmask 255.255.254.0 metric 1 mtu 1500" @@ -96,6 +106,16 @@ describe Chef::Provider::Ifconfig do expect(@new_resource).to be_updated end + it "should set the address to target if specified" do + allow(@provider).to receive(:load_current_resource) + @new_resource.target "172.16.32.2" + command = "ifconfig eth0 172.16.32.2 netmask 255.255.254.0 metric 1 mtu 1500" + expect(@provider).to receive(:run_command).with(:command => command) + + @provider.run_action(:enable) + expect(@new_resource).to be_updated + end + it "should not enable interface if it already exists" do allow(@provider).to receive(:load_current_resource) expect(@provider).not_to receive(:run_command) diff --git a/spec/unit/provider/mount/aix_spec.rb b/spec/unit/provider/mount/aix_spec.rb index ca0ddd006c..e232592275 100644 --- a/spec/unit/provider/mount/aix_spec.rb +++ b/spec/unit/provider/mount/aix_spec.rb @@ -126,9 +126,10 @@ ENABLED @provider.run_action(:mount) end - it "should not mount resource if it is already mounted" do + it "should not mount resource if it is already mounted and the options have not changed" do stub_mounted_enabled(@provider, @mounted_output, "") + allow(@provider).to receive(:mount_options_unchanged?).and_return(true) expect(@provider).not_to receive(:mount_fs) @provider.run_action(:mount) diff --git a/spec/unit/provider/mount/mount_spec.rb b/spec/unit/provider/mount/mount_spec.rb index 7a37ffe74e..dd13a62796 100644 --- a/spec/unit/provider/mount/mount_spec.rb +++ b/spec/unit/provider/mount/mount_spec.rb @@ -323,6 +323,12 @@ describe Chef::Provider::Mount::Mount do @provider.mount_fs() end + it "should not mount the filesystem if it is mounted and the options have not changed" do + allow(@current_resource).to receive(:mounted).and_return(true) + expect(@provider).to_not receive(:shell_out!) + @provider.mount_fs() + end + end describe "umount_fs" do diff --git a/spec/unit/provider/mount/windows_spec.rb b/spec/unit/provider/mount/windows_spec.rb index 467d923c6a..2de6f11d43 100644 --- a/spec/unit/provider/mount/windows_spec.rb +++ b/spec/unit/provider/mount/windows_spec.rb @@ -111,6 +111,20 @@ describe Chef::Provider::Mount::Windows do allow(@current_resource).to receive(:mounted).and_return(true) @provider.mount_fs end + + it "should remount the filesystem if it is mounted and the options have changed" do + expect(@vol).to receive(:add).with(:remote => @new_resource.device, + :username => @new_resource.username, + :domainname => @new_resource.domain, + :password => @new_resource.password) + @provider.mount_fs + end + + it "should not mount the filesystem if it is mounted and the options have not changed" do + expect(@vol).to_not receive(:add) + allow(@current_resource).to receive(:mounted).and_return(true) + @provider.mount_fs + end end describe "when unmounting a file system" do diff --git a/spec/unit/provider/mount_spec.rb b/spec/unit/provider/mount_spec.rb index e9fe3fa050..cc2a456440 100644 --- a/spec/unit/provider/mount_spec.rb +++ b/spec/unit/provider/mount_spec.rb @@ -61,8 +61,19 @@ describe Chef::Provider::Mount do expect(new_resource).to be_updated_by_last_action end - it "should not mount the filesystem if it is mounted" do + it "should remount the filesystem if it is mounted and the options have changed" do allow(current_resource).to receive(:mounted).and_return(true) + allow(provider).to receive(:mount_options_unchanged?).and_return(false) + expect(provider).to receive(:umount_fs).and_return(true) + expect(provider).to receive(:wait_until_unmounted) + expect(provider).to receive(:mount_fs).and_return(true) + provider.run_action(:mount) + expect(new_resource).to be_updated_by_last_action + end + + it "should not mount the filesystem if it is mounted and the options have not changed" do + allow(current_resource).to receive(:mounted).and_return(true) + expect(provider).to receive(:mount_options_unchanged?).and_return(true) expect(provider).not_to receive(:mount_fs) provider.run_action(:mount) expect(new_resource).not_to be_updated_by_last_action diff --git a/spec/unit/provider/package/aix_spec.rb b/spec/unit/provider/package/aix_spec.rb index 5bc861b849..13992cb8d1 100644 --- a/spec/unit/provider/package/aix_spec.rb +++ b/spec/unit/provider/package/aix_spec.rb @@ -36,23 +36,27 @@ describe Chef::Provider::Package::Aix do @bffinfo ="/usr/lib/objrepos:samba.base:3.3.12.0::COMMITTED:I:Samba for AIX: /etc/objrepos:samba.base:3.3.12.0::COMMITTED:I:Samba for AIX:" - @status = double("Status", :stdout => "", :exitstatus => 0) + @empty_status = double("Status", :stdout => "", :exitstatus => 0) end it "should create a current resource with the name of new_resource" do - allow(@provider).to receive(:shell_out).and_return(@status) + status = double("Status", :stdout => @bffinfo, :exitstatus => 0) + expect(@provider).to receive(:shell_out).with("installp -L -d /tmp/samba.base", timeout: 900).and_return(status) + expect(@provider).to receive(:shell_out).with("lslpp -lcq samba.base", timeout: 900).and_return(@empty_status) @provider.load_current_resource expect(@provider.current_resource.name).to eq("samba.base") end it "should set the current resource bff package name to the new resource bff package name" do - allow(@provider).to receive(:shell_out).and_return(@status) + status = double("Status", :stdout => @bffinfo, :exitstatus => 0) + expect(@provider).to receive(:shell_out).with("installp -L -d /tmp/samba.base", timeout: 900).and_return(status) + expect(@provider).to receive(:shell_out).with("lslpp -lcq samba.base", timeout: 900).and_return(@empty_status) @provider.load_current_resource expect(@provider.current_resource.package_name).to eq("samba.base") end it "should raise an exception if a source is supplied but not found" do - allow(@provider).to receive(:shell_out).and_return(@status) + allow(@provider).to receive(:shell_out).and_return(@empty_status) allow(::File).to receive(:exists?).and_return(false) @provider.load_current_resource @provider.define_resource_requirements @@ -61,8 +65,8 @@ describe Chef::Provider::Package::Aix do it "should get the source package version from lslpp if provided" do status = double("Status", :stdout => @bffinfo, :exitstatus => 0) - expect(@provider).to receive(:shell_out).with("installp -L -d /tmp/samba.base").and_return(status) - expect(@provider).to receive(:shell_out).with("lslpp -lcq samba.base").and_return(@status) + expect(@provider).to receive(:shell_out).with("installp -L -d /tmp/samba.base", timeout: 900).and_return(status) + expect(@provider).to receive(:shell_out).with("lslpp -lcq samba.base", timeout: 900).and_return(@empty_status) @provider.load_current_resource expect(@provider.current_resource.package_name).to eq("samba.base") @@ -73,8 +77,8 @@ describe Chef::Provider::Package::Aix do status = double("Status", :stdout => @bffinfo, :exitstatus => 0) @stdout = StringIO.new(@bffinfo) @stdin, @stderr = StringIO.new, StringIO.new - expect(@provider).to receive(:shell_out).with("installp -L -d /tmp/samba.base").and_return(@status) - expect(@provider).to receive(:shell_out).with("lslpp -lcq samba.base").and_return(status) + expect(@provider).to receive(:shell_out).with("installp -L -d /tmp/samba.base", timeout: 900).and_return(status) + expect(@provider).to receive(:shell_out).with("lslpp -lcq samba.base", timeout: 900).and_return(status) @provider.load_current_resource expect(@provider.current_resource.version).to eq("3.3.12.0") end @@ -94,12 +98,20 @@ describe Chef::Provider::Package::Aix do end it "should return a current resource with a nil version if the package is not found" do - status = double(:stdout => "", :exitstatus => 0) - expect(@provider).to receive(:shell_out).with("installp -L -d /tmp/samba.base").and_return(status) - expect(@provider).to receive(:shell_out).with("lslpp -lcq samba.base").and_return(status) + status = double("Status", :stdout => @bffinfo, :exitstatus => 0) + expect(@provider).to receive(:shell_out).with("installp -L -d /tmp/samba.base", timeout: 900).and_return(status) + expect(@provider).to receive(:shell_out).with("lslpp -lcq samba.base", timeout: 900).and_return(@empty_status) @provider.load_current_resource expect(@provider.current_resource.version).to be_nil end + + it "should raise an exception if the source doesn't provide the requested package" do + wrongbffinfo = "/usr/lib/objrepos:openssl.base:0.9.8.2400::COMMITTED:I:Open Secure Socket Layer: +/etc/objrepos:openssl.base:0.9.8.2400::COMMITTED:I:Open Secure Socket Layer:" + status = double("Status", :stdout => wrongbffinfo, :exitstatus => 0) + expect(@provider).to receive(:shell_out).with("installp -L -d /tmp/samba.base", timeout: 900).and_return(status) + expect { @provider.load_current_resource }.to raise_error(Chef::Exceptions::Package) + end end describe "candidate_version" do @@ -125,7 +137,7 @@ describe Chef::Provider::Package::Aix do describe "install and upgrade" do it "should run installp -aYF -d with the package source to install" do - expect(@provider).to receive(:shell_out!).with("installp -aYF -d /tmp/samba.base samba.base") + expect(@provider).to receive(:shell_out!).with("installp -aYF -d /tmp/samba.base samba.base", timeout: 900) @provider.install_package("samba.base", "3.3.12.0") end @@ -133,26 +145,26 @@ describe Chef::Provider::Package::Aix do @new_resource = Chef::Resource::Package.new("/tmp/samba.base") @provider = Chef::Provider::Package::Aix.new(@new_resource, @run_context) expect(@new_resource.source).to eq("/tmp/samba.base") - expect(@provider).to receive(:shell_out!).with("installp -aYF -d /tmp/samba.base /tmp/samba.base") + expect(@provider).to receive(:shell_out!).with("installp -aYF -d /tmp/samba.base /tmp/samba.base", timeout: 900) @provider.install_package("/tmp/samba.base", "3.3.12.0") end it "should run installp with -eLogfile option." do allow(@new_resource).to receive(:options).and_return("-e/tmp/installp.log") - expect(@provider).to receive(:shell_out!).with("installp -aYF -e/tmp/installp.log -d /tmp/samba.base samba.base") + expect(@provider).to receive(:shell_out!).with("installp -aYF -e/tmp/installp.log -d /tmp/samba.base samba.base", timeout: 900) @provider.install_package("samba.base", "3.3.12.0") end end describe "remove" do it "should run installp -u samba.base to remove the package" do - expect(@provider).to receive(:shell_out!).with("installp -u samba.base") + expect(@provider).to receive(:shell_out!).with("installp -u samba.base", timeout: 900) @provider.remove_package("samba.base", "3.3.12.0") end it "should run installp -u -e/tmp/installp.log with options -e/tmp/installp.log" do allow(@new_resource).to receive(:options).and_return("-e/tmp/installp.log") - expect(@provider).to receive(:shell_out!).with("installp -u -e/tmp/installp.log samba.base") + expect(@provider).to receive(:shell_out!).with("installp -u -e/tmp/installp.log samba.base", timeout: 900) @provider.remove_package("samba.base", "3.3.12.0") end diff --git a/spec/unit/provider/package/dpkg_spec.rb b/spec/unit/provider/package/dpkg_spec.rb index 3fd86218d2..b868128147 100644 --- a/spec/unit/provider/package/dpkg_spec.rb +++ b/spec/unit/provider/package/dpkg_spec.rb @@ -51,10 +51,9 @@ describe Chef::Provider::Package::Dpkg do describe 'gets the source package version from dpkg-deb' do def check_version(version) @status = double(:stdout => "wget\t#{version}", :exitstatus => 0) - allow(@provider).to receive(:shell_out).with("dpkg-deb -W #{@new_resource.source}").and_return(@status) + allow(@provider).to receive(:shell_out).with("dpkg-deb -W #{@new_resource.source}", timeout: 900).and_return(@status) @provider.load_current_resource expect(@provider.current_resource.package_name).to eq("wget") - expect(@new_resource.version).to eq(version) expect(@provider.candidate_version).to eq(version) end @@ -83,6 +82,14 @@ describe Chef::Provider::Package::Dpkg do expect(@provider.current_resource.package_name).to eq("f.o.o-pkg++2") end + it "gets the source package version from dpkg-deb correctly when the package version has `~', `-', `+' or `.' characters" do + stdout = "b.a.r-pkg++1\t1.2.3+3141592-1ubuntu1~lucid" + status = double(:stdout => stdout, :exitstatus => 1) + allow(@provider).to receive(:shell_out).and_return(status) + @provider.load_current_resource + expect(@provider.candidate_version).to eq('1.2.3+3141592-1ubuntu1~lucid') + end + it "should raise an exception if the source is not set but we are installing" do @new_resource = Chef::Resource::Package.new("wget") @provider.new_resource = @new_resource @@ -106,7 +113,7 @@ Depends: libc6 (>= 2.8~20080505), libssl0.9.8 (>= 0.9.8f-5) Conflicts: wget-ssl DPKG_S status = double(:stdout => stdout, :exitstatus => 1) - allow(@provider).to receive(:shell_out).with("dpkg -s wget").and_return(status) + allow(@provider).to receive(:shell_out).with("dpkg -s wget", timeout: 900).and_return(status) @provider.load_current_resource expect(@provider.current_resource.version).to eq("1.11.4-1ubuntu1") diff --git a/spec/unit/provider/package/freebsd/pkg_spec.rb b/spec/unit/provider/package/freebsd/pkg_spec.rb index f67161930f..d1f5a649bc 100644 --- a/spec/unit/provider/package/freebsd/pkg_spec.rb +++ b/spec/unit/provider/package/freebsd/pkg_spec.rb @@ -77,7 +77,7 @@ describe Chef::Provider::Package::Freebsd::Pkg, "load_current_resource" do it "should return the version number when it is installed" do pkg_info = OpenStruct.new(:stdout => "zsh-4.3.6_7") - expect(@provider).to receive(:shell_out!).with('pkg_info -E "zsh*"', :env => nil, :returns => [0,1]).and_return(pkg_info) + expect(@provider).to receive(:shell_out!).with('pkg_info -E "zsh*"', env: nil, returns: [0,1], timeout: 900).and_return(pkg_info) #@provider.should_receive(:popen4).with('pkg_info -E "zsh*"').and_yield(@pid, @stdin, ["zsh-4.3.6_7"], @stderr).and_return(@status) allow(@provider).to receive(:package_name).and_return("zsh") expect(@provider.current_installed_version).to eq("4.3.6_7") @@ -85,14 +85,14 @@ describe Chef::Provider::Package::Freebsd::Pkg, "load_current_resource" do it "does not set the current version number when the package is not installed" do pkg_info = OpenStruct.new(:stdout => "") - expect(@provider).to receive(:shell_out!).with('pkg_info -E "zsh*"', :env => nil, :returns => [0,1]).and_return(pkg_info) + expect(@provider).to receive(:shell_out!).with('pkg_info -E "zsh*"', env: nil, returns: [0,1], timeout: 900).and_return(pkg_info) allow(@provider).to receive(:package_name).and_return("zsh") expect(@provider.current_installed_version).to be_nil end it "should return the port path for a valid port name" do whereis = OpenStruct.new(:stdout => "zsh: /usr/ports/shells/zsh") - expect(@provider).to receive(:shell_out!).with("whereis -s zsh", :env => nil).and_return(whereis) + expect(@provider).to receive(:shell_out!).with("whereis -s zsh", env: nil, timeout: 900).and_return(whereis) #@provider.should_receive(:popen4).with("whereis -s zsh").and_yield(@pid, @stdin, ["zsh: /usr/ports/shells/zsh"], @stderr).and_return(@status) allow(@provider).to receive(:port_name).and_return("zsh") expect(@provider.port_path).to eq("/usr/ports/shells/zsh") @@ -102,7 +102,7 @@ describe Chef::Provider::Package::Freebsd::Pkg, "load_current_resource" do it "should return the ports candidate version when given a valid port path" do allow(@provider).to receive(:port_path).and_return("/usr/ports/shells/zsh") make_v = OpenStruct.new(:stdout => "4.3.6\n", :exitstatus => 0) - expect(@provider).to receive(:shell_out!).with("make -V PORTVERSION", {:cwd=>"/usr/ports/shells/zsh", :returns=>[0, 1], :env=>nil}).and_return(make_v) + expect(@provider).to receive(:shell_out!).with("make -V PORTVERSION", {cwd: "/usr/ports/shells/zsh", returns: [0, 1], env: nil, timeout: 900}).and_return(make_v) expect(@provider.ports_candidate_version).to eq("4.3.6") end @@ -110,7 +110,7 @@ describe Chef::Provider::Package::Freebsd::Pkg, "load_current_resource" do allow(::File).to receive(:exist?).with('/usr/ports/Makefile').and_return(true) allow(@provider).to receive(:port_path).and_return("/usr/ports/shells/zsh") make_v = OpenStruct.new(:stdout => "zsh-4.3.6_7\n", :exitstatus => 0) - expect(@provider).to receive(:shell_out!).with("make -V PKGNAME", {:cwd=>"/usr/ports/shells/zsh", :env=>nil, :returns=>[0, 1]}).and_return(make_v) + expect(@provider).to receive(:shell_out!).with("make -V PKGNAME", {cwd: "/usr/ports/shells/zsh", env: nil, returns: [0, 1], timeout: 900}).and_return(make_v) #@provider.should_receive(:ports_makefile_variable_value).with("PKGNAME").and_return("zsh-4.3.6_7") expect(@provider.package_name).to eq("zsh") end @@ -127,7 +127,7 @@ describe Chef::Provider::Package::Freebsd::Pkg, "load_current_resource" do end it "should run pkg_add -r with the package name" do - expect(@provider).to receive(:shell_out!).with("pkg_add -r zsh", :env => nil).and_return(@cmd_result) + expect(@provider).to receive(:shell_out!).with("pkg_add -r zsh", env: nil, timeout: 900).and_return(@cmd_result) @provider.install_package("zsh", "4.3.6_7") end end @@ -142,7 +142,7 @@ describe Chef::Provider::Package::Freebsd::Pkg, "load_current_resource" do it "should figure out the port path from the package_name using whereis" do whereis = OpenStruct.new(:stdout => "zsh: /usr/ports/shells/zsh") - expect(@provider).to receive(:shell_out!).with("whereis -s zsh", :env=>nil).and_return(whereis) + expect(@provider).to receive(:shell_out!).with("whereis -s zsh", env: nil, timeout: 900).and_return(whereis) expect(@provider.port_path).to eq("/usr/ports/shells/zsh") end @@ -178,7 +178,7 @@ describe Chef::Provider::Package::Freebsd::Pkg, "load_current_resource" do end it "should run pkg_add -r with the package name" do - expect(@provider).to receive(:shell_out!).with("pkg_add -r ruby18-iconv", :env => nil).and_return(@install_result) + expect(@provider).to receive(:shell_out!).with("pkg_add -r ruby18-iconv", env: nil, timeout: 900).and_return(@install_result) @provider.install_package("ruby-iconv", "1.0") end end @@ -193,7 +193,7 @@ describe Chef::Provider::Package::Freebsd::Pkg, "load_current_resource" do end it "should run pkg_delete with the package name and version" do - expect(@provider).to receive(:shell_out!).with("pkg_delete zsh-4.3.6_7", :env => nil).and_return(@pkg_delete) + expect(@provider).to receive(:shell_out!).with("pkg_delete zsh-4.3.6_7", env: nil, timeout: 900).and_return(@pkg_delete) @provider.remove_package("zsh", "4.3.6_7") end end @@ -213,14 +213,14 @@ describe Chef::Provider::Package::Freebsd::Pkg, "load_current_resource" do it "should return the port path for a valid port name" do whereis = OpenStruct.new(:stdout => "bonnie++: /usr/ports/benchmarks/bonnie++") - expect(@provider).to receive(:shell_out!).with("whereis -s bonnie++", :env => nil).and_return(whereis) + expect(@provider).to receive(:shell_out!).with("whereis -s bonnie++", env: nil, timeout: 900).and_return(whereis) allow(@provider).to receive(:port_name).and_return("bonnie++") expect(@provider.port_path).to eq("/usr/ports/benchmarks/bonnie++") end it "should return the version number when it is installed" do pkg_info = OpenStruct.new(:stdout => "bonnie++-1.96") - expect(@provider).to receive(:shell_out!).with('pkg_info -E "bonnie++*"', :env => nil, :returns => [0,1]).and_return(pkg_info) + expect(@provider).to receive(:shell_out!).with('pkg_info -E "bonnie++*"', env: nil, returns: [0,1], timeout: 900).and_return(pkg_info) allow(@provider).to receive(:package_name).and_return("bonnie++") expect(@provider.current_installed_version).to eq("1.96") end @@ -253,7 +253,7 @@ describe Chef::Provider::Package::Freebsd::Pkg, "load_current_resource" do allow(@provider).to receive(:latest_link_name).and_return("perl") cmd = OpenStruct.new(:status => true) - expect(@provider).to receive(:shell_out!).with("pkg_add -r perl", :env => nil).and_return(cmd) + expect(@provider).to receive(:shell_out!).with("pkg_add -r perl", env: nil, timeout: 900).and_return(cmd) @provider.install_package("perl5.8", "5.8.8_1") end @@ -267,7 +267,7 @@ describe Chef::Provider::Package::Freebsd::Pkg, "load_current_resource" do allow(@provider).to receive(:latest_link_name).and_return("mysql50-server") cmd = OpenStruct.new(:status => true) - expect(@provider).to receive(:shell_out!).with("pkg_add -r mysql50-server", :env=>nil).and_return(cmd) + expect(@provider).to receive(:shell_out!).with("pkg_add -r mysql50-server", env: nil, timeout: 900).and_return(cmd) @provider.install_package("mysql50-server", "5.0.45_1") end end diff --git a/spec/unit/provider/package/freebsd/pkgng_spec.rb b/spec/unit/provider/package/freebsd/pkgng_spec.rb index 0c1e89c7ab..59215f855b 100644 --- a/spec/unit/provider/package/freebsd/pkgng_spec.rb +++ b/spec/unit/provider/package/freebsd/pkgng_spec.rb @@ -71,7 +71,7 @@ describe Chef::Provider::Package::Freebsd::Port do end it "should query pkg database" do - expect(@provider).to receive(:shell_out!).with('pkg info "zsh"', :env => nil, :returns => [0,70]).and_return(@pkg_info) + expect(@provider).to receive(:shell_out!).with('pkg info "zsh"', env: nil, returns: [0,70], timeout: 900).and_return(@pkg_info) expect(@provider.current_installed_version).to eq("3.1.7") end end @@ -80,14 +80,14 @@ describe Chef::Provider::Package::Freebsd::Port do describe "determining candidate version" do it "should query repository" do pkg_query = OpenStruct.new(:stdout => "5.0.5\n", :exitstatus => 0) - expect(@provider).to receive(:shell_out!).with("pkg rquery '%v' zsh", :env => nil).and_return(pkg_query) + expect(@provider).to receive(:shell_out!).with("pkg rquery '%v' zsh", env: nil, timeout: 900).and_return(pkg_query) expect(@provider.candidate_version).to eq("5.0.5") end it "should query specified repository when given option" do @provider.new_resource.options('-r LocalMirror') # This requires LocalMirror repo configuration. pkg_query = OpenStruct.new(:stdout => "5.0.3\n", :exitstatus => 0) - expect(@provider).to receive(:shell_out!).with("pkg rquery -r LocalMirror '%v' zsh", :env => nil).and_return(pkg_query) + expect(@provider).to receive(:shell_out!).with("pkg rquery -r LocalMirror '%v' zsh", env: nil, timeout: 900).and_return(pkg_query) expect(@provider.candidate_version).to eq("5.0.3") end @@ -106,7 +106,7 @@ describe Chef::Provider::Package::Freebsd::Port do it "should handle package source from file" do @provider.new_resource.source("/nas/pkg/repo/zsh-5.0.1.txz") expect(@provider).to receive(:shell_out!). - with("pkg add /nas/pkg/repo/zsh-5.0.1.txz", :env => { 'LC_ALL' => nil }). + with("pkg add /nas/pkg/repo/zsh-5.0.1.txz", env: { 'LC_ALL' => nil }, timeout: 900). and_return(@install_result) @provider.install_package("zsh", "5.0.1") end @@ -114,21 +114,21 @@ describe Chef::Provider::Package::Freebsd::Port do it "should handle package source over ftp or http" do @provider.new_resource.source("http://repo.example.com/zsh-5.0.1.txz") expect(@provider).to receive(:shell_out!). - with("pkg add http://repo.example.com/zsh-5.0.1.txz", :env => { 'LC_ALL' => nil }). + with("pkg add http://repo.example.com/zsh-5.0.1.txz", env: { 'LC_ALL' => nil }, timeout: 900). and_return(@install_result) @provider.install_package("zsh", "5.0.1") end it "should handle a package name" do expect(@provider).to receive(:shell_out!). - with("pkg install -y zsh", :env => { 'LC_ALL' => nil }).and_return(@install_result) + with("pkg install -y zsh", env: { 'LC_ALL' => nil }, timeout: 900).and_return(@install_result) @provider.install_package("zsh", "5.0.1") end it "should handle a package name with a specified repo" do @provider.new_resource.options('-r LocalMirror') # This requires LocalMirror repo configuration. expect(@provider).to receive(:shell_out!). - with("pkg install -y -r LocalMirror zsh", :env => { 'LC_ALL' => nil }).and_return(@install_result) + with("pkg install -y -r LocalMirror zsh", env: { 'LC_ALL' => nil }, timeout: 900).and_return(@install_result) @provider.install_package("zsh", "5.0.1") end end @@ -141,14 +141,14 @@ describe Chef::Provider::Package::Freebsd::Port do it "should call pkg delete" do expect(@provider).to receive(:shell_out!). - with("pkg delete -y zsh-5.0.1", :env => nil).and_return(@install_result) + with("pkg delete -y zsh-5.0.1", env: nil, timeout: 900).and_return(@install_result) @provider.remove_package("zsh", "5.0.1") end it "should not include repo option in pkg delete" do @provider.new_resource.options('-r LocalMirror') # This requires LocalMirror repo configuration. expect(@provider).to receive(:shell_out!). - with("pkg delete -y zsh-5.0.1", :env => nil).and_return(@install_result) + with("pkg delete -y zsh-5.0.1", env: nil, timeout: 900).and_return(@install_result) @provider.remove_package("zsh", "5.0.1") end end diff --git a/spec/unit/provider/package/freebsd/port_spec.rb b/spec/unit/provider/package/freebsd/port_spec.rb index 2e32e88f97..4b23575740 100644 --- a/spec/unit/provider/package/freebsd/port_spec.rb +++ b/spec/unit/provider/package/freebsd/port_spec.rb @@ -72,7 +72,7 @@ describe Chef::Provider::Package::Freebsd::Port do it "should check 'pkg_info' if system uses pkg_* tools" do allow(@new_resource).to receive(:supports_pkgng?) expect(@new_resource).to receive(:supports_pkgng?).and_return(false) - expect(@provider).to receive(:shell_out!).with('pkg_info -E "zsh*"', :env => nil, :returns => [0,1]).and_return(@pkg_info) + expect(@provider).to receive(:shell_out!).with('pkg_info -E "zsh*"', env: nil, returns: [0,1], timeout: 900).and_return(@pkg_info) expect(@provider.current_installed_version).to eq("3.1.7") end @@ -80,8 +80,8 @@ describe Chef::Provider::Package::Freebsd::Port do pkg_enabled = OpenStruct.new(:stdout => "yes\n") [1000016, 1000000, 901503, 902506, 802511].each do |__freebsd_version| @node.automatic_attrs[:os_version] = __freebsd_version - expect(@new_resource).to receive(:shell_out!).with('make -V WITH_PKGNG', :env => nil).and_return(pkg_enabled) - expect(@provider).to receive(:shell_out!).with('pkg info "zsh"', :env => nil, :returns => [0,70]).and_return(@pkg_info) + expect(@new_resource).to receive(:shell_out!).with('make -V WITH_PKGNG', env: nil).and_return(pkg_enabled) + expect(@provider).to receive(:shell_out!).with('pkg info "zsh"', env: nil, returns: [0,70], timeout: 900).and_return(@pkg_info) expect(@provider.current_installed_version).to eq("3.1.7") end end @@ -89,7 +89,7 @@ describe Chef::Provider::Package::Freebsd::Port do it "should check 'pkg info' if the freebsd version is greater than or equal to 1000017" do __freebsd_version = 1000017 @node.automatic_attrs[:os_version] = __freebsd_version - expect(@provider).to receive(:shell_out!).with('pkg info "zsh"', :env => nil, :returns => [0,70]).and_return(@pkg_info) + expect(@provider).to receive(:shell_out!).with('pkg info "zsh"', env: nil, returns: [0,70], timeout: 900).and_return(@pkg_info) expect(@provider.current_installed_version).to eq("3.1.7") end end @@ -102,7 +102,7 @@ describe Chef::Provider::Package::Freebsd::Port do it "should return candidate version if port exists" do allow(::File).to receive(:exist?).with('/usr/ports/Makefile').and_return(true) allow(@provider).to receive(:port_dir).and_return('/usr/ports/shells/zsh') - expect(@provider).to receive(:shell_out!).with("make -V PORTVERSION", :cwd => "/usr/ports/shells/zsh", :env => nil, :returns => [0,1]). + expect(@provider).to receive(:shell_out!).with("make -V PORTVERSION", cwd: "/usr/ports/shells/zsh", env: nil, returns: [0,1], timeout: 900). and_return(@port_version) expect(@provider.candidate_version).to eq("5.0.5") end @@ -127,13 +127,13 @@ describe Chef::Provider::Package::Freebsd::Port do it "should query system for path given just a name" do whereis = OpenStruct.new(:stdout => "zsh: /usr/ports/shells/zsh\n") - expect(@provider).to receive(:shell_out!).with("whereis -s zsh", :env => nil).and_return(whereis) + expect(@provider).to receive(:shell_out!).with("whereis -s zsh", env: nil, timeout: 900).and_return(whereis) expect(@provider.port_dir).to eq("/usr/ports/shells/zsh") end it "should raise exception if not found" do whereis = OpenStruct.new(:stdout => "zsh:\n") - expect(@provider).to receive(:shell_out!).with("whereis -s zsh", :env => nil).and_return(whereis) + expect(@provider).to receive(:shell_out!).with("whereis -s zsh", env: nil, timeout: 900).and_return(whereis) expect { @provider.port_dir }.to raise_error(Chef::Exceptions::Package, "Could not find port with the name zsh") end end diff --git a/spec/unit/provider/package/ips_spec.rb b/spec/unit/provider/package/ips_spec.rb index 342ac4c040..ad69dffb10 100644 --- a/spec/unit/provider/package/ips_spec.rb +++ b/spec/unit/provider/package/ips_spec.rb @@ -65,28 +65,28 @@ PKG_STATUS context "when loading current resource" do it "should create a current resource with the name of the new_resource" do - expect(@provider).to receive(:shell_out).with("pkg info #{@new_resource.package_name}").and_return(local_output) - expect(@provider).to receive(:shell_out!).with("pkg info -r #{@new_resource.package_name}").and_return(remote_output) + expect(@provider).to receive(:shell_out).with("pkg info #{@new_resource.package_name}", timeout: 900).and_return(local_output) + expect(@provider).to receive(:shell_out!).with("pkg info -r #{@new_resource.package_name}", timeout: 900).and_return(remote_output) expect(Chef::Resource::Package).to receive(:new).and_return(@current_resource) @provider.load_current_resource end it "should set the current resources package name to the new resources package name" do - expect(@provider).to receive(:shell_out).with("pkg info #{@new_resource.package_name}").and_return(local_output) - expect(@provider).to receive(:shell_out!).with("pkg info -r #{@new_resource.package_name}").and_return(remote_output) + expect(@provider).to receive(:shell_out).with("pkg info #{@new_resource.package_name}", timeout: 900).and_return(local_output) + expect(@provider).to receive(:shell_out!).with("pkg info -r #{@new_resource.package_name}", timeout: 900).and_return(remote_output) @provider.load_current_resource expect(@current_resource.package_name).to eq(@new_resource.package_name) end it "should run pkg info with the package name" do - expect(@provider).to receive(:shell_out).with("pkg info #{@new_resource.package_name}").and_return(local_output) - expect(@provider).to receive(:shell_out!).with("pkg info -r #{@new_resource.package_name}").and_return(remote_output) + expect(@provider).to receive(:shell_out).with("pkg info #{@new_resource.package_name}", timeout: 900).and_return(local_output) + expect(@provider).to receive(:shell_out!).with("pkg info -r #{@new_resource.package_name}", timeout: 900).and_return(remote_output) @provider.load_current_resource end it "should set the installed version to nil on the current resource if package state is not installed" do - expect(@provider).to receive(:shell_out).with("pkg info #{@new_resource.package_name}").and_return(local_output) - expect(@provider).to receive(:shell_out!).with("pkg info -r #{@new_resource.package_name}").and_return(remote_output) + expect(@provider).to receive(:shell_out).with("pkg info #{@new_resource.package_name}", timeout: 900).and_return(local_output) + expect(@provider).to receive(:shell_out!).with("pkg info -r #{@new_resource.package_name}", timeout: 900).and_return(remote_output) @provider.load_current_resource expect(@current_resource.version).to be_nil end @@ -108,27 +108,27 @@ Packaging Date: October 19, 2011 09:14:50 AM Size: 8.07 MB FMRI: pkg://solaris/crypto/gnupg@2.0.17,5.11-0.175.0.0.0.2.537:20111019T091450Z INSTALLED - expect(@provider).to receive(:shell_out).with("pkg info #{@new_resource.package_name}").and_return(local) - expect(@provider).to receive(:shell_out!).with("pkg info -r #{@new_resource.package_name}").and_return(remote_output) + expect(@provider).to receive(:shell_out).with("pkg info #{@new_resource.package_name}", timeout: 900).and_return(local) + expect(@provider).to receive(:shell_out!).with("pkg info -r #{@new_resource.package_name}", timeout: 900).and_return(remote_output) @provider.load_current_resource expect(@current_resource.version).to eq("2.0.17") end it "should return the current resource" do - expect(@provider).to receive(:shell_out).with("pkg info #{@new_resource.package_name}").and_return(local_output) - expect(@provider).to receive(:shell_out!).with("pkg info -r #{@new_resource.package_name}").and_return(remote_output) + expect(@provider).to receive(:shell_out).with("pkg info #{@new_resource.package_name}", timeout: 900).and_return(local_output) + expect(@provider).to receive(:shell_out!).with("pkg info -r #{@new_resource.package_name}", timeout: 900).and_return(remote_output) expect(@provider.load_current_resource).to eql(@current_resource) end end context "when installing a package" do it "should run pkg install with the package name and version" do - expect(@provider).to receive(:shell_out).with("pkg install -q crypto/gnupg@2.0.17") + expect(@provider).to receive(:shell_out).with("pkg install -q crypto/gnupg@2.0.17", timeout: 900) @provider.install_package("crypto/gnupg", "2.0.17") end it "should run pkg install with the package name and version and options if specified" do - expect(@provider).to receive(:shell_out).with("pkg --no-refresh install -q crypto/gnupg@2.0.17") + expect(@provider).to receive(:shell_out).with("pkg --no-refresh install -q crypto/gnupg@2.0.17", timeout: 900) allow(@new_resource).to receive(:options).and_return("--no-refresh") @provider.install_package("crypto/gnupg", "2.0.17") end @@ -147,8 +147,8 @@ Packaging Date: April 1, 2012 05:55:52 PM Size: 2.57 MB FMRI: pkg://omnios/security/sudo@1.8.4.1,5.11-0.151002:20120401T175552Z PKG_STATUS - expect(@provider).to receive(:shell_out).with("pkg info #{@new_resource.package_name}").and_return(local_output) - expect(@provider).to receive(:shell_out!).with("pkg info -r #{@new_resource.package_name}").and_return(remote) + expect(@provider).to receive(:shell_out).with("pkg info #{@new_resource.package_name}", timeout: 900).and_return(local_output) + expect(@provider).to receive(:shell_out!).with("pkg info -r #{@new_resource.package_name}", timeout: 900).and_return(remote) @provider.load_current_resource expect(@current_resource.version).to be_nil expect(@provider.candidate_version).to eql("1.8.4.1") @@ -188,8 +188,8 @@ Packaging Date: October 19, 2011 09:14:50 AM FMRI: pkg://solaris/crypto/gnupg@2.0.18,5.11-0.175.0.0.0.2.537:20111019T091450Z REMOTE - expect(@provider).to receive(:shell_out).with("pkg info #{@new_resource.package_name}").and_return(local) - expect(@provider).to receive(:shell_out!).with("pkg info -r #{@new_resource.package_name}").and_return(remote) + expect(@provider).to receive(:shell_out).with("pkg info #{@new_resource.package_name}", timeout: 900).and_return(local) + expect(@provider).to receive(:shell_out!).with("pkg info -r #{@new_resource.package_name}", timeout: 900).and_return(remote) expect(@provider).to receive(:install_package).exactly(0).times @provider.run_action(:install) end @@ -200,7 +200,7 @@ REMOTE end it "should run pkg install with the --accept flag" do - expect(@provider).to receive(:shell_out).with("pkg install -q --accept crypto/gnupg@2.0.17") + expect(@provider).to receive(:shell_out).with("pkg install -q --accept crypto/gnupg@2.0.17", timeout: 900) @provider.install_package("crypto/gnupg", "2.0.17") end end @@ -208,19 +208,19 @@ REMOTE context "when upgrading a package" do it "should run pkg install with the package name and version" do - expect(@provider).to receive(:shell_out).with("pkg install -q crypto/gnupg@2.0.17") + expect(@provider).to receive(:shell_out).with("pkg install -q crypto/gnupg@2.0.17", timeout: 900) @provider.upgrade_package("crypto/gnupg", "2.0.17") end end context "when uninstalling a package" do it "should run pkg uninstall with the package name and version" do - expect(@provider).to receive(:shell_out!).with("pkg uninstall -q crypto/gnupg@2.0.17") + expect(@provider).to receive(:shell_out!).with("pkg uninstall -q crypto/gnupg@2.0.17", timeout: 900) @provider.remove_package("crypto/gnupg", "2.0.17") end it "should run pkg uninstall with the package name and version and options if specified" do - expect(@provider).to receive(:shell_out!).with("pkg --no-refresh uninstall -q crypto/gnupg@2.0.17") + expect(@provider).to receive(:shell_out!).with("pkg --no-refresh uninstall -q crypto/gnupg@2.0.17", timeout: 900) allow(@new_resource).to receive(:options).and_return("--no-refresh") @provider.remove_package("crypto/gnupg", "2.0.17") end diff --git a/spec/unit/provider/package/macports_spec.rb b/spec/unit/provider/package/macports_spec.rb index 9822fb3928..eef84113b4 100644 --- a/spec/unit/provider/package/macports_spec.rb +++ b/spec/unit/provider/package/macports_spec.rb @@ -105,7 +105,7 @@ EOF it "should run the port install command with the correct version" do expect(@current_resource).to receive(:version).and_return("4.1.6") @provider.current_resource = @current_resource - expect(@provider).to receive(:shell_out!).with("port install zsh @4.2.7") + expect(@provider).to receive(:shell_out!).with("port install zsh @4.2.7", timeout: 900) @provider.install_package("zsh", "4.2.7") end @@ -122,7 +122,7 @@ EOF expect(@current_resource).to receive(:version).and_return("4.1.6") @provider.current_resource = @current_resource allow(@new_resource).to receive(:options).and_return("-f") - expect(@provider).to receive(:shell_out!).with("port -f install zsh @4.2.7") + expect(@provider).to receive(:shell_out!).with("port -f install zsh @4.2.7", timeout: 900) @provider.install_package("zsh", "4.2.7") end @@ -130,36 +130,36 @@ EOF describe "purge_package" do it "should run the port uninstall command with the correct version" do - expect(@provider).to receive(:shell_out!).with("port uninstall zsh @4.2.7") + expect(@provider).to receive(:shell_out!).with("port uninstall zsh @4.2.7", timeout: 900) @provider.purge_package("zsh", "4.2.7") end it "should purge the currently active version if no explicit version is passed in" do - expect(@provider).to receive(:shell_out!).with("port uninstall zsh") + expect(@provider).to receive(:shell_out!).with("port uninstall zsh", timeout: 900) @provider.purge_package("zsh", nil) end it "should add options to the port command when specified" do allow(@new_resource).to receive(:options).and_return("-f") - expect(@provider).to receive(:shell_out!).with("port -f uninstall zsh @4.2.7") + expect(@provider).to receive(:shell_out!).with("port -f uninstall zsh @4.2.7", timeout: 900) @provider.purge_package("zsh", "4.2.7") end end describe "remove_package" do it "should run the port deactivate command with the correct version" do - expect(@provider).to receive(:shell_out!).with("port deactivate zsh @4.2.7") + expect(@provider).to receive(:shell_out!).with("port deactivate zsh @4.2.7", timeout: 900) @provider.remove_package("zsh", "4.2.7") end it "should remove the currently active version if no explicit version is passed in" do - expect(@provider).to receive(:shell_out!).with("port deactivate zsh") + expect(@provider).to receive(:shell_out!).with("port deactivate zsh", timeout: 900) @provider.remove_package("zsh", nil) end it "should add options to the port command when specified" do allow(@new_resource).to receive(:options).and_return("-f") - expect(@provider).to receive(:shell_out!).with("port -f deactivate zsh @4.2.7") + expect(@provider).to receive(:shell_out!).with("port -f deactivate zsh @4.2.7", timeout: 900) @provider.remove_package("zsh", "4.2.7") end end @@ -169,7 +169,7 @@ EOF expect(@current_resource).to receive(:version).at_least(:once).and_return("4.1.6") @provider.current_resource = @current_resource - expect(@provider).to receive(:shell_out!).with("port upgrade zsh @4.2.7") + expect(@provider).to receive(:shell_out!).with("port upgrade zsh @4.2.7", timeout: 900) @provider.upgrade_package("zsh", "4.2.7") end @@ -195,7 +195,7 @@ EOF expect(@current_resource).to receive(:version).at_least(:once).and_return("4.1.6") @provider.current_resource = @current_resource - expect(@provider).to receive(:shell_out!).with("port -f upgrade zsh @4.2.7") + expect(@provider).to receive(:shell_out!).with("port -f upgrade zsh @4.2.7", timeout: 900) @provider.upgrade_package("zsh", "4.2.7") end diff --git a/spec/unit/provider/package/openbsd_spec.rb b/spec/unit/provider/package/openbsd_spec.rb index ee9c9e89fb..8407f83785 100644 --- a/spec/unit/provider/package/openbsd_spec.rb +++ b/spec/unit/provider/package/openbsd_spec.rb @@ -21,28 +21,95 @@ require 'ostruct' describe Chef::Provider::Package::Openbsd do + let(:node) do + node = Chef::Node.new + node.default['kernel'] = {'name' => 'OpenBSD', 'release' => '5.5', 'machine' => 'amd64'} + node + end + + let (:provider) do + events = Chef::EventDispatch::Dispatcher.new + run_context = Chef::RunContext.new(node, {}, events) + Chef::Provider::Package::Openbsd.new(new_resource, run_context) + end + + let(:new_resource) { Chef::Resource::Package.new(name)} + before(:each) do - @node = Chef::Node.new - @node.default['kernel'] = {'name' => 'OpenBSD', 'release' => '5.5', 'machine' => 'amd64'} - @events = Chef::EventDispatch::Dispatcher.new - @run_context = Chef::RunContext.new(@node, {}, @events) ENV['PKG_PATH'] = nil end describe "install a package" do - before do - @name = 'ihavetoes' - @new_resource = Chef::Resource::Package.new(@name) - @current_resource = Chef::Resource::Package.new(@name) - @provider = Chef::Provider::Package::Openbsd.new(@new_resource, @run_context) - @provider.current_resource = @current_resource - end - it "should run the installation command" do - expect(@provider).to receive(:shell_out!).with( - "pkg_add -r #{@name}", - {:env => {"PKG_PATH" => "http://ftp.OpenBSD.org/pub/OpenBSD/5.5/packages/amd64/"}} - ) {OpenStruct.new :status => true} - @provider.install_package(@name, nil) + let(:name) { 'ihavetoes' } + let(:version) {'0.0'} + + context 'when not already installed' do + before do + allow(provider).to receive(:shell_out!).with("pkg_info -e \"#{name}->0\"", anything()).and_return(instance_double('shellout', :stdout => '')) + end + + context 'when there is a single candidate' do + + context 'when source is not provided' do + it 'should run the installation command' do + expect(provider).to receive(:shell_out!).with("pkg_info -I \"#{name}\"", anything()).and_return( + instance_double('shellout', :stdout => "#{name}-#{version}\n")) + expect(provider).to receive(:shell_out!).with( + "pkg_add -r #{name}-#{version}", + {:env => {"PKG_PATH" => "http://ftp.OpenBSD.org/pub/OpenBSD/5.5/packages/amd64/"}, timeout: 900} + ) {OpenStruct.new :status => true} + provider.run_action(:install) + end + end + end + + context 'when there are multiple candidates' do + let(:flavor_a) { 'flavora' } + let(:flavor_b) { 'flavorb' } + + context 'if no version is specified' do + it 'should raise an exception' do + expect(provider).to receive(:shell_out!).with("pkg_info -I \"#{name}\"", anything()).and_return( + instance_double('shellout', :stdout => "#{name}-#{version}-#{flavor_a}\n#{name}-#{version}-#{flavor_b}\n")) + expect { provider.run_action(:install) }.to raise_error(Chef::Exceptions::Package, /multiple matching candidates/) + end + end + + context 'if a flavor is specified' do + + let(:flavor) { 'flavora' } + let(:package_name) {'ihavetoes' } + let(:name) { "#{package_name}--#{flavor}" } + + context 'if no version is specified' do + it 'should run the installation command' do + expect(provider).to receive(:shell_out!).with("pkg_info -e \"#{package_name}->0\"", anything()).and_return(instance_double('shellout', :stdout => '')) + expect(provider).to receive(:shell_out!).with("pkg_info -I \"#{name}\"", anything()).and_return( + instance_double('shellout', :stdout => "#{name}-#{version}-#{flavor}\n")) + expect(provider).to receive(:shell_out!).with( + "pkg_add -r #{name}-#{version}-#{flavor}", + {env: {"PKG_PATH" => "http://ftp.OpenBSD.org/pub/OpenBSD/5.5/packages/amd64/"}, timeout: 900} + ) {OpenStruct.new :status => true} + provider.run_action(:install) + end + end + + end + + context 'if a version is specified' do + it 'should use the flavor from the version' do + expect(provider).to receive(:shell_out!).with("pkg_info -I \"#{name}-#{version}-#{flavor_b}\"", anything()).and_return( + instance_double('shellout', :stdout => "#{name}-#{version}-#{flavor_a}\n")) + + new_resource.version("#{version}-#{flavor_b}") + expect(provider).to receive(:shell_out!).with( + "pkg_add -r #{name}-#{version}-#{flavor_b}", + {env: {"PKG_PATH" => "http://ftp.OpenBSD.org/pub/OpenBSD/5.5/packages/amd64/"}, timeout: 900} + ) {OpenStruct.new :status => true} + provider.run_action(:install) + end + end + end end end @@ -56,11 +123,10 @@ describe Chef::Provider::Package::Openbsd do end it "should run the command to delete the installed package" do expect(@provider).to receive(:shell_out!).with( - "pkg_delete #{@name}", :env=>nil + "pkg_delete #{@name}", env: nil, timeout: 900 ) {OpenStruct.new :status => true} @provider.remove_package(@name, nil) end end end - diff --git a/spec/unit/provider/package/pacman_spec.rb b/spec/unit/provider/package/pacman_spec.rb index 3b8848c41b..fcb9f8a86c 100644 --- a/spec/unit/provider/package/pacman_spec.rb +++ b/spec/unit/provider/package/pacman_spec.rb @@ -51,7 +51,7 @@ ERR end it "should run pacman query with the package name" do - expect(@provider).to receive(:shell_out).with("pacman -Qi #{@new_resource.package_name}").and_return(@status) + expect(@provider).to receive(:shell_out).with("pacman -Qi #{@new_resource.package_name}", {timeout: 900}).and_return(@status) @provider.load_current_resource end @@ -152,12 +152,12 @@ PACMAN_CONF describe Chef::Provider::Package::Pacman, "install_package" do it "should run pacman install with the package name and version" do - expect(@provider).to receive(:shell_out!).with("pacman --sync --noconfirm --noprogressbar nano") + expect(@provider).to receive(:shell_out!).with("pacman --sync --noconfirm --noprogressbar nano", {timeout: 900}) @provider.install_package("nano", "1.0") end it "should run pacman install with the package name and version and options if specified" do - expect(@provider).to receive(:shell_out!).with("pacman --sync --noconfirm --noprogressbar --debug nano") + expect(@provider).to receive(:shell_out!).with("pacman --sync --noconfirm --noprogressbar --debug nano", {timeout: 900}) allow(@new_resource).to receive(:options).and_return("--debug") @provider.install_package("nano", "1.0") @@ -173,12 +173,12 @@ PACMAN_CONF describe Chef::Provider::Package::Pacman, "remove_package" do it "should run pacman remove with the package name" do - expect(@provider).to receive(:shell_out!).with("pacman --remove --noconfirm --noprogressbar nano") + expect(@provider).to receive(:shell_out!).with("pacman --remove --noconfirm --noprogressbar nano", {timeout: 900}) @provider.remove_package("nano", "1.0") end it "should run pacman remove with the package name and options if specified" do - expect(@provider).to receive(:shell_out!).with("pacman --remove --noconfirm --noprogressbar --debug nano") + expect(@provider).to receive(:shell_out!).with("pacman --remove --noconfirm --noprogressbar --debug nano", {timeout: 900}) allow(@new_resource).to receive(:options).and_return("--debug") @provider.remove_package("nano", "1.0") diff --git a/spec/unit/provider/package/rpm_spec.rb b/spec/unit/provider/package/rpm_spec.rb index 411afd3755..e0e45d0b4f 100644 --- a/spec/unit/provider/package/rpm_spec.rb +++ b/spec/unit/provider/package/rpm_spec.rb @@ -23,183 +23,394 @@ describe Chef::Provider::Package::Rpm do let(:node) { Chef::Node.new } let(:events) { Chef::EventDispatch::Dispatcher.new } let(:run_context) { Chef::RunContext.new(node, {}, events) } + + let(:package_source) { "/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm" } + + let(:package_name) { "ImageMagick-c++" } + let(:new_resource) do - Chef::Resource::Package.new("ImageMagick-c++").tap do |resource| - resource.source "/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm" + Chef::Resource::Package.new(package_name).tap do |resource| + resource.source(package_source) end end - let(:exitstatus) { 0 } - let(:stdout) { String.new('') } - let(:status) { double('Process::Status', exitstatus: exitstatus, stdout: stdout) } + + # `rpm -qp [stuff] $source` + let(:rpm_qp_status) { instance_double('Mixlib::ShellOut', exitstatus: rpm_qp_exitstatus, stdout: rpm_qp_stdout) } + + # `rpm -q [stuff] $package_name` + let(:rpm_q_status) { instance_double('Mixlib::ShellOut', exitstatus: rpm_q_exitstatus, stdout: rpm_q_stdout) } before(:each) do - allow(::File).to receive(:exists?).and_return(true) - allow(provider).to receive(:shell_out!).and_return(status) + allow(::File).to receive(:exists?).with("PLEASE STUB File.exists? EXACTLY").and_return(true) + + # Ensure all shell out usage is stubbed with exact arguments + allow(provider).to receive(:shell_out!).with("PLEASE STUB YOUR SHELLOUT CALLS").and_return(nil) + allow(provider).to receive(:shell_out).with("PLEASE STUB YOUR SHELLOUT CALLS").and_return(nil) end - describe "when determining the current state of the package" do - it "should create a current resource with the name of new_resource" do - provider.load_current_resource - expect(provider.current_resource.name).to eq("ImageMagick-c++") - end + describe "when the package source is not valid" do - it "should set the current reource package name to the new resource package name" do - provider.load_current_resource - expect(provider.current_resource.package_name).to eq('ImageMagick-c++') - end + context "when source is not defiend" do + let(:new_resource) { Chef::Resource::Package.new("ImageMagick-c++") } - it "should raise an exception if a source is supplied but not found" do - allow(::File).to receive(:exists?).and_return(false) - expect { provider.run_action(:any) }.to raise_error(Chef::Exceptions::Package) + it "should raise an exception when attempting any action" do + expect { provider.run_action(:any) }.to raise_error(Chef::Exceptions::Package) + end end - context "installation exists" do - let(:stdout) { "ImageMagick-c++ 6.5.4.7-7.el6_5" } + context "when the source is a file that doesn't exist" do - it "should get the source package version from rpm if provided" do - expect(provider).to receive(:shell_out!).with("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm").and_return(status) - expect(provider).to receive(:shell_out).with("rpm -q --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' ImageMagick-c++").and_return(status) - provider.load_current_resource - expect(provider.current_resource.package_name).to eq("ImageMagick-c++") - expect(provider.new_resource.version).to eq("6.5.4.7-7.el6_5") + it "should raise an exception when attempting any action" do + allow(::File).to receive(:exists?).with(package_source).and_return(false) + expect { provider.run_action(:any) }.to raise_error(Chef::Exceptions::Package) end + end - it "should return the current version installed if found by rpm" do - expect(provider).to receive(:shell_out!).with("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm").and_return(status) - expect(provider).to receive(:shell_out).with("rpm -q --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' ImageMagick-c++").and_return(status) - provider.load_current_resource - expect(provider.current_resource.version).to eq("6.5.4.7-7.el6_5") + context "when the source is an unsupported URI scheme" do + + let(:package_source) { "foobar://example.com/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm" } + + it "should raise an exception if an uri formed source is non-supported scheme" do + allow(::File).to receive(:exists?).with(package_source).and_return(false) + + # verify let bindings are as we expect + expect(new_resource.source).to eq("foobar://example.com/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") + expect(provider.load_current_resource).to be_nil + expect { provider.run_action(:any) }.to raise_error(Chef::Exceptions::Package) end end - context "source is uri formed" do - before(:each) do - allow(::File).to receive(:exists?).and_return(false) + end + + describe "when the package source is valid" do + + before do + expect(provider).to receive(:shell_out!). + with("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{package_source}", timeout: 900). + and_return(rpm_qp_status) + + expect(provider).to receive(:shell_out). + with("rpm -q --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{package_name}", timeout: 900). + and_return(rpm_q_status) + end + + context "when rpm fails when querying package installed state" do + + before do + allow(::File).to receive(:exists?).with(package_source).and_return(true) end - %w(http HTTP https HTTPS ftp FTP).each do |scheme| - it "should accept uri formed source (#{scheme})" do - new_resource.source "#{scheme}://example.com/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm" - expect(provider.load_current_resource).not_to be_nil + let(:rpm_qp_stdout) { "ImageMagick-c++ 6.5.4.7-7.el6_5" } + let(:rpm_q_stdout) { "" } + + let(:rpm_qp_exitstatus) { 0 } + let(:rpm_q_exitstatus) { -1 } + + it "raises an exception when attempting any action" do + expected_message = "Unable to determine current version due to RPM failure." + + expect { provider.run_action(:install) }.to raise_error do |error| + expect(error).to be_a_kind_of(Chef::Exceptions::Package) + expect(error.to_s).to include(expected_message) end end + end + + + context "when the package is installed" do + + let(:rpm_qp_stdout) { "ImageMagick-c++ 6.5.4.7-7.el6_5" } + let(:rpm_q_stdout) { "ImageMagick-c++ 6.5.4.7-7.el6_5" } + + let(:rpm_qp_exitstatus) { 0 } + let(:rpm_q_exitstatus) { 0 } - %w(file FILE).each do |scheme| - it "should accept uri formed source (#{scheme})" do - new_resource.source "#{scheme}:///ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm" - expect(provider.load_current_resource).not_to be_nil + let(:action) { :install } + + context "when the source is a file system path" do + + before do + allow(::File).to receive(:exists?).with(package_source).and_return(true) + + provider.action = action + + provider.load_current_resource + provider.define_resource_requirements + provider.process_resource_requirements end - end - it "should raise an exception if an uri formed source is non-supported scheme" do - new_resource.source "foobar://example.com/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm" - expect(provider.load_current_resource).to be_nil - expect { provider.run_action(:any) }.to raise_error(Chef::Exceptions::Package) - end - end + it "should get the source package version from rpm if provided" do + expect(provider.current_resource.package_name).to eq("ImageMagick-c++") + expect(provider.new_resource.version).to eq("6.5.4.7-7.el6_5") + end - context "source is not defiend" do - let(:new_resource) { Chef::Resource::Package.new("ImageMagick-c++") } + it "should return the current version installed if found by rpm" do + expect(provider.current_resource.version).to eq("6.5.4.7-7.el6_5") + end + + describe "action install" do + + context "when at the desired version already" do + it "does nothing when the correct version is installed" do + expect(provider).to_not receive(:shell_out!).with("rpm -i /tmp/imagemagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", timeout: 900) + + provider.action_install + end + end + + context "when a newer version is desired" do + + let(:rpm_q_stdout) { "imagemagick-c++ 0.5.4.7-7.el6_5" } + + it "runs rpm -u with the package source to upgrade" do + expect(provider).to receive(:shell_out!).with("rpm -U /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", timeout: 900) + provider.action_install + end + end + + context "when an older version is desired" do + let(:new_resource) do + Chef::Resource::RpmPackage.new(package_name).tap do |r| + r.source(package_source) + r.allow_downgrade(true) + end + end + + let(:rpm_q_stdout) { "imagemagick-c++ 21.4-19.el6_5" } + + it "should run rpm -u --oldpackage with the package source to downgrade" do + expect(provider).to receive(:shell_out!).with("rpm -U --oldpackage /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", timeout: 900) + provider.action_install + end + + end + + end + + describe "action upgrade" do + + let(:action) { :upgrade } + + context "when at the desired version already" do + it "does nothing when the correct version is installed" do + expect(provider).to_not receive(:shell_out!).with("rpm -i /tmp/imagemagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", timeout: 900) + + provider.action_upgrade + end + end + + context "when a newer version is desired" do + + let(:rpm_q_stdout) { "imagemagick-c++ 0.5.4.7-7.el6_5" } + + it "runs rpm -u with the package source to upgrade" do + expect(provider).to receive(:shell_out!).with("rpm -U /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", timeout: 900) + provider.action_upgrade + end + end + + context "when an older version is desired" do + let(:new_resource) do + Chef::Resource::RpmPackage.new(package_name).tap do |r| + r.source(package_source) + r.allow_downgrade(true) + end + end + + let(:rpm_q_stdout) { "imagemagick-c++ 21.4-19.el6_5" } + + it "should run rpm -u --oldpackage with the package source to downgrade" do + expect(provider).to receive(:shell_out!).with("rpm -U --oldpackage /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", timeout: 900) + provider.action_upgrade + end + + end + end + + describe "action :remove" do + + let(:action) { :remove } + + it "should remove the package" do + expect(provider).to receive(:shell_out!).with("rpm -e ImageMagick-c++-6.5.4.7-7.el6_5", timeout: 900) + provider.action_remove + end + end + + + context "when the package name contains a tilde (chef#3503)" do + + let(:package_name) { "supermarket" } + + let(:package_source) { "/tmp/supermarket-1.10.1~alpha.0-1.el5.x86_64.rpm" } + + let(:rpm_qp_stdout) { "supermarket 1.10.1~alpha.0-1.el5" } + let(:rpm_q_stdout) { "supermarket 1.10.1~alpha.0-1.el5" } + + let(:rpm_qp_exitstatus) { 0 } + let(:rpm_q_exitstatus) { 0 } + + it "should correctly determine the candidate version and installed version" do + expect(provider.current_resource.package_name).to eq("supermarket") + expect(provider.new_resource.version).to eq("1.10.1~alpha.0-1.el5") + end + end - it "should raise an exception if the source is not set but we are installing" do - expect { provider.run_action(:any) }.to raise_error(Chef::Exceptions::Package) end - end - context "installation does not exist" do - let(:stdout) { String.new("package openssh-askpass is not installed") } - let(:exitstatus) { -1 } - let(:new_resource) do - Chef::Resource::Package.new("openssh-askpass").tap do |resource| - resource.source "openssh-askpass" + context "when the source is given as an URI" do + before(:each) do + allow(::File).to receive(:exists?).with(package_source).and_return(false) + + provider.action = action + + provider.load_current_resource + provider.define_resource_requirements + provider.process_resource_requirements + end + + %w(http HTTP https HTTPS ftp FTP file FILE).each do |scheme| + + context "when the source URI uses protocol scheme '#{scheme}'" do + + let(:package_source) { "#{scheme}://example.com/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm" } + + it "should get the source package version from rpm if provided" do + expect(provider.current_resource.package_name).to eq("ImageMagick-c++") + expect(provider.new_resource.version).to eq("6.5.4.7-7.el6_5") + end + + it "should return the current version installed if found by rpm" do + expect(provider.current_resource.version).to eq("6.5.4.7-7.el6_5") + end + + end end + end - it "should raise an exception if rpm fails to run" do - allow(provider).to receive(:shell_out).and_return(status) - expect { provider.run_action(:any) }.to raise_error(Chef::Exceptions::Package) + end + + context "when the package is not installed" do + + let(:package_name) { "openssh-askpass" } + + let(:package_source) { "/tmp/openssh-askpass-1.2.3-4.el6_5.x86_64.rpm" } + + let(:rpm_qp_stdout) { "openssh-askpass 1.2.3-4.el6_5" } + let(:rpm_q_stdout) { "package openssh-askpass is not installed" } + + let(:rpm_qp_exitstatus) { 0 } + let(:rpm_q_exitstatus) { 0 } + + let(:action) { :install } + + before do + allow(File).to receive(:exists?).with(package_source).and_return(true) + + provider.action = action + + provider.load_current_resource + provider.define_resource_requirements + provider.process_resource_requirements end it "should not detect the package name as version when not installed" do - expect(provider).to receive(:shell_out!).with("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' openssh-askpass").and_return(status) - expect(provider).to receive(:shell_out).with("rpm -q --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' openssh-askpass").and_return(status) - provider.load_current_resource expect(provider.current_resource.version).to be_nil end - end - end - describe "after the current resource is loaded" do - let(:current_resource) { Chef::Resource::Package.new("ImageMagick-c++") } - let(:provider) do - Chef::Provider::Package::Rpm.new(new_resource, run_context).tap do |provider| - provider.current_resource = current_resource - end - end + context "when the package name contains a tilde (chef#3503)" do - describe "when installing or upgrading" do - it "should run rpm -i with the package source to install" do - expect(provider).to receive(:shell_out!).with("rpm -i /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") - provider.install_package("ImageMagick-c++", "6.5.4.7-7.el6_5") - end + let(:package_name) { "supermarket" } - it "should run rpm -U with the package source to upgrade" do - current_resource.version("21.4-19.el5") - expect(provider).to receive(:shell_out!).with("rpm -U /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") - provider.upgrade_package("ImageMagick-c++", "6.5.4.7-7.el6_5") - end + let(:package_source) { "/tmp/supermarket-1.10.1~alpha.0-1.el5.x86_64.rpm" } - it "should install package if missing and set to upgrade" do - current_resource.version("ImageMagick-c++") - expect(provider).to receive(:shell_out!).with("rpm -U /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") - provider.upgrade_package("ImageMagick-c++", "6.5.4.7-7.el6_5") - end + let(:rpm_qp_stdout) { "supermarket 1.10.1~alpha.0-1.el5" } + let(:rpm_q_stdout) { "package supermarket is not installed" } - context "allowing downgrade" do - let(:new_resource) { Chef::Resource::RpmPackage.new("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") } - let(:current_resource) { Chef::Resource::RpmPackage.new("ImageMagick-c++") } + let(:rpm_qp_exitstatus) { 0 } + let(:rpm_q_exitstatus) { 0 } - it "should run rpm -U --oldpackage with the package source to downgrade" do - new_resource.allow_downgrade(true) - current_resource.version("21.4-19.el5") - expect(provider).to receive(:shell_out!).with("rpm -U --oldpackage /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") - provider.upgrade_package("ImageMagick-c++", "6.5.4.7-7.el6_5") + it "should correctly determine the candidate version" do + expect(provider.new_resource.version).to eq("1.10.1~alpha.0-1.el5") end end - context "installing when the name is a path" do - let(:new_resource) { Chef::Resource::Package.new("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") } - let(:current_resource) { Chef::Resource::Package.new("ImageMagick-c++") } + describe "managing the package" do + + describe "action install" do + + it "installs the package" do + expect(provider).to receive(:shell_out!).with("rpm -i #{package_source}", timeout: 900) - it "should install from a path when the package is a path and the source is nil" do - expect(new_resource.source).to eq("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") - provider.current_resource = current_resource - expect(provider).to receive(:shell_out!).with("rpm -i /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") - provider.install_package("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", "6.5.4.7-7.el6_5") + provider.action_install + end + + context "when custom resource options are given" do + it "installs with custom options specified in the resource" do + new_resource.options("--dbpath /var/lib/rpm") + expect(provider).to receive(:shell_out!).with("rpm --dbpath /var/lib/rpm -i #{package_source}", timeout: 900) + provider.action_install + end + end end - it "should uprgrade from a path when the package is a path and the source is nil" do - expect(new_resource.source).to eq("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") - current_resource.version("21.4-19.el5") - provider.current_resource = current_resource - expect(provider).to receive(:shell_out!).with("rpm -U /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") - provider.upgrade_package("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", "6.5.4.7-7.el6_5") + describe "action upgrade" do + + let(:action) { :upgrade } + + it "installs the package" do + expect(provider).to receive(:shell_out!).with("rpm -i #{package_source}", timeout: 900) + + provider.action_upgrade + end + end + + describe "when removing the package" do + + let(:action) { :remove } + + it "should do nothing" do + expect(provider).to_not receive(:shell_out!).with("rpm -e ImageMagick-c++-6.5.4.7-7.el6_5", timeout: 900) + provider.action_remove + end end - end - it "installs with custom options specified in the resource" do - provider.candidate_version = '11' - new_resource.options("--dbpath /var/lib/rpm") - expect(provider).to receive(:shell_out!).with("rpm --dbpath /var/lib/rpm -i /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") - provider.install_package(new_resource.name, provider.candidate_version) end + + end + end - describe "when removing the package" do - it "should run rpm -e to remove the package" do - expect(provider).to receive(:shell_out!).with("rpm -e ImageMagick-c++-6.5.4.7-7.el6_5") - provider.remove_package("ImageMagick-c++", "6.5.4.7-7.el6_5") - end + context "when the resource name is the path to the package" do + + let(:new_resource) do + # When we pass a source in as the name, then #initialize in the + # provider will call File.exists?. Because of the ordering in our + # let() bindings and such, we have to set the stub here and not in a + # before block. + allow(::File).to receive(:exists?).with(package_source).and_return(true) + Chef::Resource::Package.new("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") + end + + let(:current_resource) { Chef::Resource::Package.new("ImageMagick-c++") } + + it "should install from a path when the package is a path and the source is nil" do + expect(new_resource.source).to eq("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") + provider.current_resource = current_resource + expect(provider).to receive(:shell_out!).with("rpm -i /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", timeout: 900) + provider.install_package("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", "6.5.4.7-7.el6_5") + end + + it "should uprgrade from a path when the package is a path and the source is nil" do + expect(new_resource.source).to eq("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") + current_resource.version("21.4-19.el5") + provider.current_resource = current_resource + expect(provider).to receive(:shell_out!).with("rpm -U /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", timeout: 900) + provider.upgrade_package("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", "6.5.4.7-7.el6_5") end end + + end + diff --git a/spec/unit/provider/package/rubygems_spec.rb b/spec/unit/provider/package/rubygems_spec.rb index b17c216ddd..f790bdb1ce 100644 --- a/spec/unit/provider/package/rubygems_spec.rb +++ b/spec/unit/provider/package/rubygems_spec.rb @@ -107,38 +107,6 @@ describe Chef::Provider::Package::Rubygems::CurrentGemEnvironment do expect(@gem_env.candidate_version_from_remote(Gem::Dependency.new('rspec', '>= 0'))).to eq(Gem::Version.new('1.3.0')) end - context "when rubygems was upgraded from 1.8->2.0" do - # https://github.com/rubygems/rubygems/issues/404 - # tl;dr rubygems 1.8 and 2.0 can both be in the load path, which means that - # require "rubygems/format" will load even though rubygems 2.0 doesn't have - # that file. - - before do - if defined?(Gem::Format) - # tests are running under rubygems 1.8, or 2.0 upgraded from 1.8 - @remove_gem_format = false - else - Gem.const_set(:Format, Object.new) - @remove_gem_format = true - end - allow(Gem::Package).to receive(:respond_to?).and_call_original - allow(Gem::Package).to receive(:respond_to?).with(:open).and_return(false) - end - - after do - if @remove_gem_format - Gem.send(:remove_const, :Format) - end - end - - it "finds a matching gem candidate version on rubygems 2.0+ with some rubygems 1.8 code loaded" do - package = double("Gem::Package", :spec => "a gemspec from package") - expect(Gem::Package).to receive(:new).with("/path/to/package.gem").and_return(package) - expect(@gem_env.spec_from_file("/path/to/package.gem")).to eq("a gemspec from package") - end - - end - it "gives the candidate version as nil if none is found" do dep = Gem::Dependency.new('rspec', '>= 0') latest = [] @@ -222,8 +190,6 @@ describe Chef::Provider::Package::Rubygems::AlternateGemEnvironment do end it "uses the cached result for gem paths when available" do - gem_env_output = ['/path/to/gems', '/another/path/to/gems'].join(File::PATH_SEPARATOR) - shell_out_result = OpenStruct.new(:stdout => gem_env_output) expect(@gem_env).not_to receive(:shell_out!) expected = ['/path/to/gems', '/another/path/to/gems'] Chef::Provider::Package::Rubygems::AlternateGemEnvironment.gempath_cache['/usr/weird/bin/gem']= expected @@ -261,7 +227,7 @@ describe Chef::Provider::Package::Rubygems::AlternateGemEnvironment do else `which gem`.strip end - pending("cant find your gem executable") if path_to_gem.empty? + skip("cant find your gem executable") if path_to_gem.empty? gem_env = Chef::Provider::Package::Rubygems::AlternateGemEnvironment.new(path_to_gem) expected = ['rspec-core', Gem::Version.new(RSpec::Core::Version::STRING)] actual = gem_env.installed_versions(Gem::Dependency.new('rspec-core', nil)).map { |s| [s.name, s.version] } @@ -403,6 +369,24 @@ describe Chef::Provider::Package::Rubygems do expect(provider.gem_env.gem_binary_location).to eq('/usr/weird/bin/gem') end + it "recognizes chef as omnibus" do + allow(RbConfig::CONFIG).to receive(:[]).with('bindir').and_return("/opt/chef/embedded/bin") + provider = Chef::Provider::Package::Rubygems.new(@new_resource, @run_context) + expect(provider.is_omnibus?).to be true + end + + it "recognizes opscode as omnibus" do + allow(RbConfig::CONFIG).to receive(:[]).with('bindir').and_return("/opt/opscode/embedded/bin") + provider = Chef::Provider::Package::Rubygems.new(@new_resource, @run_context) + expect(provider.is_omnibus?).to be true + end + + it "recognizes chefdk as omnibus" do + allow(RbConfig::CONFIG).to receive(:[]).with('bindir').and_return("/opt/chefdk/embedded/bin") + provider = Chef::Provider::Package::Rubygems.new(@new_resource, @run_context) + expect(provider.is_omnibus?).to be true + end + it "searches for a gem binary when running on Omnibus on Unix" do platform_mock :unix do allow(RbConfig::CONFIG).to receive(:[]).with('bindir').and_return("/opt/chef/embedded/bin") @@ -542,7 +526,26 @@ describe Chef::Provider::Package::Rubygems do it "installs the gem by shelling out when options are provided as a String" do @new_resource.options('-i /alt/install/location') expected ="gem install rspec-core -q --no-rdoc --no-ri -v \"#{@spec_version}\" -i /alt/install/location" - expect(@provider).to receive(:shell_out!).with(expected, :env => nil) + expect(@provider).to receive(:shell_out!).with(expected, env: nil, timeout: 900) + @provider.run_action(:install) + expect(@new_resource).to be_updated_by_last_action + end + + it "installs the gem with rubygems.org as an added source" do + @new_resource.gem_binary('/foo/bar') + @new_resource.source('http://mirror.ops.rhcloud.com/mirror/ruby') + expected ="/foo/bar install rspec-core -q --no-rdoc --no-ri -v \"#{@spec_version}\" --source=#{@new_resource.source} --source=https://rubygems.org" + expect(@provider).to receive(:shell_out!).with(expected, env: nil, timeout: 900) + @provider.run_action(:install) + expect(@new_resource).to be_updated_by_last_action + end + + it "installs the gem with cleared sources and explict source when specified" do + @new_resource.gem_binary('/foo/bar') + @new_resource.source('http://mirror.ops.rhcloud.com/mirror/ruby') + @new_resource.clear_sources(true) + expected ="/foo/bar install rspec-core -q --no-rdoc --no-ri -v \"#{@spec_version}\" --clear-sources --source=#{@new_resource.source}" + expect(@provider).to receive(:shell_out!).with(expected, env: nil, timeout: 900) @provider.run_action(:install) expect(@new_resource).to be_updated_by_last_action end @@ -553,7 +556,7 @@ describe Chef::Provider::Package::Rubygems do it "installs the gem by shelling out when options are provided but no version is given" do @new_resource.options('-i /alt/install/location') expected ="gem install rspec-core -q --no-rdoc --no-ri -v \"#{@provider.candidate_version}\" -i /alt/install/location" - expect(@provider).to receive(:shell_out!).with(expected, :env => nil) + expect(@provider).to receive(:shell_out!).with(expected, env: nil, timeout: 900) @provider.run_action(:install) expect(@new_resource).to be_updated_by_last_action end @@ -599,7 +602,7 @@ describe Chef::Provider::Package::Rubygems do describe "in an alternate gem environment" do it "installs the gem by shelling out to gem install" do @new_resource.gem_binary('/usr/weird/bin/gem') - expect(@provider).to receive(:shell_out!).with("/usr/weird/bin/gem install rspec-core -q --no-rdoc --no-ri -v \"#{@spec_version}\"", :env=>nil) + expect(@provider).to receive(:shell_out!).with("/usr/weird/bin/gem install rspec-core -q --no-rdoc --no-ri -v \"#{@spec_version}\"", env: nil, timeout: 900) @provider.run_action(:install) expect(@new_resource).to be_updated_by_last_action end @@ -608,7 +611,7 @@ describe Chef::Provider::Package::Rubygems do @new_resource.gem_binary('/usr/weird/bin/gem') @new_resource.source(CHEF_SPEC_DATA + '/gems/chef-integration-test-0.1.0.gem') @new_resource.version('>= 0') - expect(@provider).to receive(:shell_out!).with("/usr/weird/bin/gem install #{CHEF_SPEC_DATA}/gems/chef-integration-test-0.1.0.gem -q --no-rdoc --no-ri -v \">= 0\"", :env=>nil) + expect(@provider).to receive(:shell_out!).with("/usr/weird/bin/gem install #{CHEF_SPEC_DATA}/gems/chef-integration-test-0.1.0.gem -q --no-rdoc --no-ri -v \">= 0\"", env: nil, timeout: 900) @provider.run_action(:install) expect(@new_resource).to be_updated_by_last_action end @@ -620,7 +623,7 @@ describe Chef::Provider::Package::Rubygems do @new_resource.gem_binary('/usr/weird/bin/gem') @new_resource.version('>= 0') expect(@new_resource.source).to eq(CHEF_SPEC_DATA + '/gems/chef-integration-test-0.1.0.gem') - expect(@provider).to receive(:shell_out!).with("/usr/weird/bin/gem install #{CHEF_SPEC_DATA}/gems/chef-integration-test-0.1.0.gem -q --no-rdoc --no-ri -v \">= 0\"", :env=>nil) + expect(@provider).to receive(:shell_out!).with("/usr/weird/bin/gem install #{CHEF_SPEC_DATA}/gems/chef-integration-test-0.1.0.gem -q --no-rdoc --no-ri -v \">= 0\"", env: nil, timeout: 900) @provider.run_action(:install) expect(@new_resource).to be_updated_by_last_action end @@ -659,7 +662,7 @@ describe Chef::Provider::Package::Rubygems do it "uninstalls via the gem command when options are given as a String" do @new_resource.options('-i /alt/install/location') - expect(@provider).to receive(:shell_out!).with("gem uninstall rspec -q -x -I -a -i /alt/install/location", :env=>nil) + expect(@provider).to receive(:shell_out!).with("gem uninstall rspec -q -x -I -a -i /alt/install/location", env: nil, timeout: 900) @provider.action_remove end @@ -673,7 +676,7 @@ describe Chef::Provider::Package::Rubygems do describe "in an alternate gem environment" do it "uninstalls via the gem command" do @new_resource.gem_binary('/usr/weird/bin/gem') - expect(@provider).to receive(:shell_out!).with("/usr/weird/bin/gem uninstall rspec -q -x -I -a", :env=>nil) + expect(@provider).to receive(:shell_out!).with("/usr/weird/bin/gem uninstall rspec -q -x -I -a", env: nil, timeout: 900) @provider.action_remove end end diff --git a/spec/unit/provider/package/smartos_spec.rb b/spec/unit/provider/package/smartos_spec.rb index db39589b85..8f2d2bb8ea 100644 --- a/spec/unit/provider/package/smartos_spec.rb +++ b/spec/unit/provider/package/smartos_spec.rb @@ -29,45 +29,45 @@ describe Chef::Provider::Package::SmartOS, "load_current_resource" do @current_resource = Chef::Resource::Package.new("varnish") - @status = double("Status", :exitstatus => 0) - @provider = Chef::Provider::Package::SmartOS.new(@new_resource, @run_context) - allow(Chef::Resource::Package).to receive(:new).and_return(@current_resource) - @stdin = StringIO.new - @stdout = "varnish-2.1.5nb2\n" - @stderr = StringIO.new - @pid = 10 - @shell_out = OpenStruct.new(:stdout => @stdout, :stdin => @stdin, :stderr => @stderr, :status => @status, :exitstatus => 0) + @status = double("Status", :exitstatus => 0) + @provider = Chef::Provider::Package::SmartOS.new(@new_resource, @run_context) + allow(Chef::Resource::Package).to receive(:new).and_return(@current_resource) + @stdin = StringIO.new + @stdout = "varnish-2.1.5nb2\n" + @stderr = StringIO.new + @pid = 10 + @shell_out = OpenStruct.new(:stdout => @stdout, :stdin => @stdin, :stderr => @stderr, :status => @status, :exitstatus => 0) end - describe "when loading current resource" do + describe "when loading current resource" do it "should create a current resource with the name of the new_resource" do - expect(@provider).to receive(:shell_out!).and_return(@shell_out) - expect(Chef::Resource::Package).to receive(:new).and_return(@current_resource) - @provider.load_current_resource + expect(@provider).to receive(:shell_out!).and_return(@shell_out) + expect(Chef::Resource::Package).to receive(:new).and_return(@current_resource) + @provider.load_current_resource end - it "should set the current resource package name" do - expect(@provider).to receive(:shell_out!).and_return(@shell_out) - expect(@current_resource).to receive(:package_name).with(@new_resource.package_name) - @provider.load_current_resource - end + it "should set the current resource package name" do + expect(@provider).to receive(:shell_out!).and_return(@shell_out) + expect(@current_resource).to receive(:package_name).with(@new_resource.package_name) + @provider.load_current_resource + end - it "should set the installed version if it is installed" do - expect(@provider).to receive(:shell_out!).and_return(@shell_out) - @provider.load_current_resource - expect(@current_resource.version).to eq("2.1.5nb2") - end + it "should set the installed version if it is installed" do + expect(@provider).to receive(:shell_out!).and_return(@shell_out) + @provider.load_current_resource + expect(@current_resource.version).to eq("2.1.5nb2") + end - it "should set the installed version to nil if it's not installed" do - out = OpenStruct.new(:stdout => nil) - expect(@provider).to receive(:shell_out!).and_return(out) - @provider.load_current_resource - expect(@current_resource.version).to eq(nil) - end + it "should set the installed version to nil if it's not installed" do + out = OpenStruct.new(:stdout => nil) + expect(@provider).to receive(:shell_out!).and_return(out) + @provider.load_current_resource + expect(@current_resource.version).to eq(nil) + end - end + end describe "candidate_version" do it "should return the candidate_version variable if already setup" do @@ -76,27 +76,37 @@ describe Chef::Provider::Package::SmartOS, "load_current_resource" do @provider.candidate_version end - it "should lookup the candidate_version if the variable is not already set" do + it "should lookup the candidate_version if the variable is not already set (pkgin separated by spaces)" do search = double() expect(search).to receive(:each_line). - and_yield("something-varnish-1.1.1 something varnish like\n"). - and_yield("varnish-2.3.4 actual varnish\n") + and_yield("something-varnish-1.1.1 something varnish like\n"). + and_yield("varnish-2.3.4 actual varnish\n") @shell_out = double('shell_out!', :stdout => search) - expect(@provider).to receive(:shell_out!).with('/opt/local/bin/pkgin se varnish', :env => nil, :returns => [0,1]).and_return(@shell_out) + expect(@provider).to receive(:shell_out!).with('/opt/local/bin/pkgin', 'se', 'varnish', :env => nil, :returns => [0,1], :timeout=>900).and_return(@shell_out) + expect(@provider.candidate_version).to eq("2.3.4") + end + + it "should lookup the candidate_version if the variable is not already set (pkgin separated by semicolons)" do + search = double() + expect(search).to receive(:each_line). + and_yield("something-varnish-1.1.1;;something varnish like\n"). + and_yield("varnish-2.3.4;;actual varnish\n") + @shell_out = double('shell_out!', :stdout => search) + expect(@provider).to receive(:shell_out!).with('/opt/local/bin/pkgin', 'se', 'varnish', :env => nil, :returns => [0,1], :timeout=>900).and_return(@shell_out) expect(@provider.candidate_version).to eq("2.3.4") end end - describe "when manipulating a resource" do + describe "when manipulating a resource" do - it "run pkgin and install the package" do - out = OpenStruct.new(:stdout => nil) - expect(@provider).to receive(:shell_out!).with("/opt/local/sbin/pkg_info -E \"varnish*\"", {:env => nil, :returns=>[0,1]}).and_return(@shell_out) - expect(@provider).to receive(:shell_out!).with("/opt/local/bin/pkgin -y install varnish-2.1.5nb2", {:env=>nil}).and_return(out) + it "run pkgin and install the package" do + out = OpenStruct.new(:stdout => nil) + expect(@provider).to receive(:shell_out!).with("/opt/local/sbin/pkg_info", "-E", "varnish*", {:env => nil, :returns=>[0,1], :timeout=>900}).and_return(@shell_out) + expect(@provider).to receive(:shell_out!).with("/opt/local/bin/pkgin", "-y", "install", "varnish-2.1.5nb2", {:env=>nil, :timeout=>900}).and_return(out) @provider.load_current_resource @provider.install_package("varnish", "2.1.5nb2") - end + end - end + end end diff --git a/spec/unit/provider/package/solaris_spec.rb b/spec/unit/provider/package/solaris_spec.rb index c348d665e8..ae6c96da00 100644 --- a/spec/unit/provider/package/solaris_spec.rb +++ b/spec/unit/provider/package/solaris_spec.rb @@ -71,8 +71,8 @@ PKGINFO it "should get the source package version from pkginfo if provided" do status = double(:stdout => @pkginfo, :exitstatus => 0) - expect(@provider).to receive(:shell_out).with("pkginfo -l -d /tmp/bash.pkg SUNWbash").and_return(status) - expect(@provider).to receive(:shell_out).with("pkginfo -l SUNWbash").and_return(@status) + expect(@provider).to receive(:shell_out).with("pkginfo -l -d /tmp/bash.pkg SUNWbash", { timeout: 900 }).and_return(status) + expect(@provider).to receive(:shell_out).with("pkginfo -l SUNWbash", { timeout: 900 }).and_return(@status) @provider.load_current_resource expect(@provider.current_resource.package_name).to eq("SUNWbash") @@ -81,8 +81,8 @@ PKGINFO it "should return the current version installed if found by pkginfo" do status = double(:stdout => @pkginfo, :exitstatus => 0) - expect(@provider).to receive(:shell_out).with("pkginfo -l -d /tmp/bash.pkg SUNWbash").and_return(@status) - expect(@provider).to receive(:shell_out).with("pkginfo -l SUNWbash").and_return(status) + expect(@provider).to receive(:shell_out).with("pkginfo -l -d /tmp/bash.pkg SUNWbash", { timeout: 900 }).and_return(@status) + expect(@provider).to receive(:shell_out).with("pkginfo -l SUNWbash", { timeout: 900 }).and_return(status) @provider.load_current_resource expect(@provider.current_resource.version).to eq("11.10.0,REV=2005.01.08.05.16") end @@ -101,8 +101,8 @@ PKGINFO end it "should return a current resource with a nil version if the package is not found" do - expect(@provider).to receive(:shell_out).with("pkginfo -l -d /tmp/bash.pkg SUNWbash").and_return(@status) - expect(@provider).to receive(:shell_out).with("pkginfo -l SUNWbash").and_return(@status) + expect(@provider).to receive(:shell_out).with("pkginfo -l -d /tmp/bash.pkg SUNWbash", { timeout: 900 }).and_return(@status) + expect(@provider).to receive(:shell_out).with("pkginfo -l SUNWbash", { timeout: 900 }).and_return(@status) @provider.load_current_resource expect(@provider.current_resource.version).to be_nil end @@ -132,7 +132,7 @@ PKGINFO describe "install and upgrade" do it "should run pkgadd -n -d with the package source to install" do - expect(@provider).to receive(:shell_out!).with("pkgadd -n -d /tmp/bash.pkg all") + expect(@provider).to receive(:shell_out!).with("pkgadd -n -d /tmp/bash.pkg all", { timeout: 900 }) @provider.install_package("SUNWbash", "11.10.0,REV=2005.01.08.05.16") end @@ -140,26 +140,26 @@ PKGINFO @new_resource = Chef::Resource::Package.new("/tmp/bash.pkg") @provider = Chef::Provider::Package::Solaris.new(@new_resource, @run_context) expect(@new_resource.source).to eq("/tmp/bash.pkg") - expect(@provider).to receive(:shell_out!).with("pkgadd -n -d /tmp/bash.pkg all") + expect(@provider).to receive(:shell_out!).with("pkgadd -n -d /tmp/bash.pkg all", { timeout: 900 }) @provider.install_package("/tmp/bash.pkg", "11.10.0,REV=2005.01.08.05.16") end it "should run pkgadd -n -a /tmp/myadmin -d with the package options -a /tmp/myadmin" do allow(@new_resource).to receive(:options).and_return("-a /tmp/myadmin") - expect(@provider).to receive(:shell_out!).with("pkgadd -n -a /tmp/myadmin -d /tmp/bash.pkg all") + expect(@provider).to receive(:shell_out!).with("pkgadd -n -a /tmp/myadmin -d /tmp/bash.pkg all", { timeout: 900 }) @provider.install_package("SUNWbash", "11.10.0,REV=2005.01.08.05.16") end end describe "remove" do it "should run pkgrm -n to remove the package" do - expect(@provider).to receive(:shell_out!).with("pkgrm -n SUNWbash") + expect(@provider).to receive(:shell_out!).with("pkgrm -n SUNWbash", { timeout: 900 }) @provider.remove_package("SUNWbash", "11.10.0,REV=2005.01.08.05.16") end it "should run pkgrm -n -a /tmp/myadmin with options -a /tmp/myadmin" do allow(@new_resource).to receive(:options).and_return("-a /tmp/myadmin") - expect(@provider).to receive(:shell_out!).with("pkgrm -n -a /tmp/myadmin SUNWbash") + expect(@provider).to receive(:shell_out!).with("pkgrm -n -a /tmp/myadmin SUNWbash", { timeout: 900 }) @provider.remove_package("SUNWbash", "11.10.0,REV=2005.01.08.05.16") end diff --git a/spec/unit/provider/package/windows_spec.rb b/spec/unit/provider/package/windows_spec.rb index d402113d72..e5acc87694 100644 --- a/spec/unit/provider/package/windows_spec.rb +++ b/spec/unit/provider/package/windows_spec.rb @@ -19,50 +19,129 @@ require 'spec_helper' describe Chef::Provider::Package::Windows, :windows_only do + before(:each) do + allow(Chef::Util::PathHelper).to receive(:windows?).and_return(true) + allow(Chef::FileCache).to receive(:create_cache_path).with("package/").and_return(cache_path) + end + 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(:resource_source) { 'calculator.msi' } + let(:new_resource) { Chef::Resource::WindowsPackage.new(resource_source) } let(:provider) { Chef::Provider::Package::Windows.new(new_resource, run_context) } + let(:cache_path) { 'c:\\cache\\' } describe "load_current_resource" do - before(:each) do - allow(Chef::Util::PathHelper).to receive(:validate_path) - allow(provider).to receive(:package_provider).and_return(double('package_provider', + shared_examples "a local file" do + before(:each) do + allow(Chef::Util::PathHelper).to receive(:validate_path) + allow(provider).to receive(:package_provider).and_return(double('package_provider', :installed_version => "1.0", :package_version => "2.0")) - end + 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 "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) + 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 current version if the package is installed" do - provider.load_current_resource - expect(provider.current_resource.version).to eql("1.0") + it "sets the version to be installed" do + provider.load_current_resource + expect(provider.new_resource.version).to eql("2.0") + end end - it "sets the version to be installed" do - provider.load_current_resource - expect(provider.new_resource.version).to eql("2.0") + context "when the source is a uri" do + let(:resource_source) { 'https://foo.bar/calculator.msi' } + + context "when the source has not been downloaded" do + before(:each) do + allow(provider).to receive(:downloadable_file_missing?).and_return(true) + end + it "sets the current version to unknown" do + provider.load_current_resource + expect(provider.current_resource.version).to eql("unknown") + end + end + + context "when the source has been downloaded" do + before(:each) do + allow(provider).to receive(:downloadable_file_missing?).and_return(false) + 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 - it "checks that the source path is valid" do - expect(Chef::Util::PathHelper).to receive(:validate_path) - provider.load_current_resource + context "when source is a local file" do + it_behaves_like "a local file" end end describe "package_provider" do - it "sets the package provider to MSI if the the installer type is :msi" do - allow(provider).to receive(:installer_type).and_return(:msi) - expect(provider.package_provider).to be_a(Chef::Provider::Package::Windows::MSI) + 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 + end + + it "sets the package provider to MSI if the the installer type is :msi" do + allow(provider).to receive(: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 + allow(provider).to receive(:installer_type).and_return(:apt_for_windows) + expect { provider.package_provider }.to raise_error + end + end + + context "when the source is a uri" do + let(:resource_source) { 'https://foo.bar/calculator.msi' } + + context "when the source has not been downloaded" do + before(:each) do + allow(provider).to receive(:should_download?).and_return(true) + end + + it "should create a package provider with source pointing at the local file" do + expect(Chef::Provider::Package::Windows::MSI).to receive(:new) do |r| + expect(r.source).to eq("#{cache_path}#{::File.basename(resource_source)}") + end + provider.package_provider + end + + it_behaves_like "a local file" + end + + context "when the source has been downloaded" do + before(:each) do + allow(provider).to receive(:should_download?).and_return(false) + end + it_behaves_like "a local file" + end 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 + context "when source is a local file" do + it_behaves_like "a local file" end end diff --git a/spec/unit/provider/package/yum_spec.rb b/spec/unit/provider/package/yum_spec.rb index cd2b3decf4..3fc0b807c9 100644 --- a/spec/unit/provider/package/yum_spec.rb +++ b/spec/unit/provider/package/yum_spec.rb @@ -1,6 +1,6 @@ # # Author:: Adam Jacob (<adam@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Copyright:: Copyright (c) 2008-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,13 +17,14 @@ # require 'spec_helper' +require 'securerandom' describe Chef::Provider::Package::Yum do before(:each) do @node = Chef::Node.new @events = Chef::EventDispatch::Dispatcher.new @run_context = Chef::RunContext.new(@node, {}, @events) - @new_resource = Chef::Resource::Package.new('cups') + @new_resource = Chef::Resource::YumPackage.new('cups') @status = double("Status", :exitstatus => 0) @yum_cache = double( 'Chef::Provider::Yum::YumCache', @@ -38,6 +39,7 @@ describe Chef::Provider::Package::Yum do :disable_extra_repo_control => true ) allow(Chef::Provider::Package::Yum::YumCache).to receive(:instance).and_return(@yum_cache) + allow(@yum_cache).to receive(:yum_binary=).with("yum") @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) @pid = double("PID") end @@ -73,6 +75,60 @@ describe Chef::Provider::Package::Yum do expect(@provider.load_current_resource).to eql(@provider.current_resource) end + describe "when source is provided" do + it "should set the candidate version" do + @new_resource = Chef::Resource::YumPackage.new('testing.source') + @new_resource.source "chef-server-core-12.0.5-1.rpm" + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + allow(File).to receive(:exists?).with(@new_resource.source).and_return(true) + allow(@yum_cache).to receive(:installed_version).and_return(nil) + shellout_double = double(:stdout => 'chef-server-core 12.0.5-1') + allow(@provider).to receive(:shell_out!).and_return(shellout_double) + @provider.load_current_resource + expect(@provider.candidate_version).to eql('12.0.5-1') + end + end + + describe "yum_binary accessor" do + it "when yum-deprecated exists" do + expect(File).to receive(:exist?).with("/usr/bin/yum-deprecated").and_return(true) + expect(@yum_cache).to receive(:yum_binary=).with("yum-deprecated") + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + expect(@provider.yum_binary).to eql("yum-deprecated") + end + + it "when yum-deprecated does not exist" do + expect(File).to receive(:exist?).with("/usr/bin/yum-deprecated").and_return(false) + expect(@yum_cache).to receive(:yum_binary=).with("yum") + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + expect(@provider.yum_binary).to eql("yum") + end + + it "when the yum_binary is set on the resource" do + @new_resource.yum_binary "/usr/bin/yum-something" + expect(File).not_to receive(:exist?) + expect(@yum_cache).to receive(:yum_binary=).with("/usr/bin/yum-something") + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + expect(@provider.yum_binary).to eql("/usr/bin/yum-something") + end + + it "when the new_resource is a vanilla package class and yum-deprecated exists" do + @new_resource = Chef::Resource::Package.new('cups') + expect(File).to receive(:exist?).with("/usr/bin/yum-deprecated").and_return(true) + expect(@yum_cache).to receive(:yum_binary=).with("yum-deprecated") + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + expect(@provider.yum_binary).to eql("yum-deprecated") + end + + it "when the new_resource is a vanilla package class and yum-deprecated does not exist" do + @new_resource = Chef::Resource::Package.new('cups') + expect(File).to receive(:exist?).with("/usr/bin/yum-deprecated").and_return(false) + expect(@yum_cache).to receive(:yum_binary=).with("yum") + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + expect(@provider.yum_binary).to eql("yum") + end + end + describe "when arch in package_name" do it "should set the arch if no existing package_name is found and new_package_name+new_arch is available" do @new_resource = Chef::Resource::YumPackage.new('testing.noarch') @@ -94,6 +150,7 @@ describe Chef::Provider::Package::Yum do allow(@yum_cache).to receive(:package_available?).and_return(true) allow(@yum_cache).to receive(:disable_extra_repo_control).and_return(true) allow(Chef::Provider::Package::Yum::YumCache).to receive(:instance).and_return(@yum_cache) + allow(@yum_cache).to receive(:yum_binary=).with("yum") @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) @provider.load_current_resource expect(@provider.new_resource.package_name).to eq("testing") @@ -108,6 +165,26 @@ describe Chef::Provider::Package::Yum do expect(@provider.arch).to eq("noarch") end + describe "when version constraint in package_name" do + it "should set package_version if no existing package_name is found and new_package_name is available" do + @new_resource = Chef::Resource::Package.new('cups = 1.2.4-11.18.el5_2.3') + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + allow(@yum_cache).to receive(:package_available?) { |pkg| pkg == 'cups' ? true : false } + allow(@yum_cache).to receive(:packages_from_require) do |pkg| + [Chef::Provider::Package::Yum::RPMDbPackage.new("cups", "1.2.4-11.18.el5_2.3", "noarch", [], false, true, "base"), + Chef::Provider::Package::Yum::RPMDbPackage.new("cups", "1.2.4-11.18.el5_2.2", "noarch", [], false, true, "base"),] + end + expect(Chef::Log).to receive(:debug).exactly(1).times.with(%r{checking yum info}) + expect(Chef::Log).to receive(:debug).exactly(1).times.with(%r{installed version}) + expect(Chef::Log).to receive(:debug).exactly(1).times.with(%r{matched 2 packages,}) + @provider.load_current_resource + expect(@provider.new_resource.package_name).to eq("cups") + expect(@provider.new_resource.version).to eq("1.2.4-11.18.el5_2.3") + expect(@provider.send(:new_version_array)).to eq(["1.2.4-11.18.el5_2.3"]) + expect(@provider.send(:package_name_array)).to eq(["cups"]) + end + end + it "should not set the arch when an existing package_name is found" do @new_resource = Chef::Resource::YumPackage.new('testing.beta3') @yum_cache = double( @@ -128,6 +205,7 @@ describe Chef::Provider::Package::Yum do allow(@yum_cache).to receive(:package_available?).and_return(true) allow(@yum_cache).to receive(:disable_extra_repo_control).and_return(true) allow(Chef::Provider::Package::Yum::YumCache).to receive(:instance).and_return(@yum_cache) + allow(@yum_cache).to receive(:yum_binary=).with("yum") @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) # annoying side effect of the fun stub'ing above @provider.load_current_resource @@ -159,6 +237,7 @@ describe Chef::Provider::Package::Yum do allow(@yum_cache).to receive(:package_available?).and_return(true) allow(@yum_cache).to receive(:disable_extra_repo_control).and_return(true) allow(Chef::Provider::Package::Yum::YumCache).to receive(:instance).and_return(@yum_cache) + allow(@yum_cache).to receive(:yum_binary=).with("yum") @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) @provider.load_current_resource expect(@provider.new_resource.package_name).to eq("testing.beta3") @@ -194,6 +273,7 @@ describe Chef::Provider::Package::Yum do allow(@yum_cache).to receive(:package_available?).and_return(true) allow(@yum_cache).to receive(:disable_extra_repo_control).and_return(true) allow(Chef::Provider::Package::Yum::YumCache).to receive(:instance).and_return(@yum_cache) + allow(@yum_cache).to receive(:yum_binary=).with("yum") @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) @provider.load_current_resource expect(@provider.new_resource.package_name).to eq("testing.i386") @@ -246,6 +326,7 @@ describe Chef::Provider::Package::Yum do before do allow(Chef::Provider::Package::Yum::YumCache).to receive(:instance).and_return(yum_cache) + allow(yum_cache).to receive(:yum_binary=).with("yum") @pkg = Chef::Provider::Package::Yum::RPMPackage.new("test-package", "2.0.1.el5", "x86_64", []) expect(yum_cache).to receive(:packages_from_require).and_return([@pkg]) end @@ -317,6 +398,7 @@ describe Chef::Provider::Package::Yum do :disable_extra_repo_control => true ) allow(Chef::Provider::Package::Yum::YumCache).to receive(:instance).and_return(@yum_cache) + allow(@yum_cache).to receive(:yum_binary=).with("yum") pkg = Chef::Provider::Package::Yum::RPMPackage.new("test-package", "2.0.1.el5", "x86_64", []) expect(@yum_cache).to receive(:packages_from_require).and_return([pkg]) @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) @@ -338,6 +420,7 @@ describe Chef::Provider::Package::Yum do :disable_extra_repo_control => true ) allow(Chef::Provider::Package::Yum::YumCache).to receive(:instance).and_return(@yum_cache) + allow(@yum_cache).to receive(:yum_binary=).with("yum") pkg = Chef::Provider::Package::Yum::RPMPackage.new("test-package", "2.0.1.el5", "x86_64", []) expect(@yum_cache).to receive(:packages_from_require).and_return([pkg]) @new_resource = Chef::Resource::YumPackage.new('test-package = 2.0.1.el5') @@ -360,6 +443,7 @@ describe Chef::Provider::Package::Yum do :disable_extra_repo_control => true ) allow(Chef::Provider::Package::Yum::YumCache).to receive(:instance).and_return(@yum_cache) + allow(@yum_cache).to receive(:yum_binary=).with("yum") expect(@yum_cache).to receive(:packages_from_require).exactly(4).times.and_return([]) expect(@yum_cache).to receive(:reload_provides).twice @@ -384,6 +468,7 @@ describe Chef::Provider::Package::Yum do :disable_extra_repo_control => true ) allow(Chef::Provider::Package::Yum::YumCache).to receive(:instance).and_return(@yum_cache) + allow(@yum_cache).to receive(:yum_binary=).with("yum") expect(@yum_cache).to receive(:packages_from_require).twice.and_return([]) expect(@yum_cache).to receive(:reload_provides) @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) @@ -403,6 +488,7 @@ describe Chef::Provider::Package::Yum do :disable_extra_repo_control => true ) allow(Chef::Provider::Package::Yum::YumCache).to receive(:instance).and_return(@yum_cache) + allow(@yum_cache).to receive(:yum_binary=).with("yum") @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) expect(@yum_cache).to receive(:packages_from_require).once.and_return([]) expect(@yum_cache).not_to receive(:reload_provides) @@ -427,6 +513,7 @@ describe Chef::Provider::Package::Yum do :disable_extra_repo_control => true ) allow(Chef::Provider::Package::Yum::YumCache).to receive(:instance).and_return(@yum_cache) + allow(@yum_cache).to receive(:yum_binary=).with("yum") expect(@yum_cache).to receive(:packages_from_require).twice.and_return([]) @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) @provider.load_current_resource @@ -439,7 +526,7 @@ describe Chef::Provider::Package::Yum do @provider.load_current_resource allow(Chef::Provider::Package::Yum::RPMUtils).to receive(:rpmvercmp).and_return(-1) expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y install cups-1.2.4-11.19.el5" + "-d0 -e0 -y install cups-1.2.4-11.19.el5" ) @provider.install_package("cups", "1.2.4-11.19.el5") end @@ -447,7 +534,7 @@ describe Chef::Provider::Package::Yum do it "should run yum localinstall if given a path to an rpm" do allow(@new_resource).to receive(:source).and_return("/tmp/emacs-21.4-20.el5.i386.rpm") expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y localinstall /tmp/emacs-21.4-20.el5.i386.rpm" + "-d0 -e0 -y localinstall /tmp/emacs-21.4-20.el5.i386.rpm" ) @provider.install_package("emacs", "21.4-20.el5") end @@ -458,7 +545,7 @@ describe Chef::Provider::Package::Yum do @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) expect(@new_resource.source).to eq("/tmp/emacs-21.4-20.el5.i386.rpm") expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y localinstall /tmp/emacs-21.4-20.el5.i386.rpm" + "-d0 -e0 -y localinstall /tmp/emacs-21.4-20.el5.i386.rpm" ) @provider.install_package("/tmp/emacs-21.4-20.el5.i386.rpm", "21.4-20.el5") end @@ -468,7 +555,7 @@ describe Chef::Provider::Package::Yum do allow(@new_resource).to receive(:arch).and_return("i386") allow(Chef::Provider::Package::Yum::RPMUtils).to receive(:rpmvercmp).and_return(-1) expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y install cups-1.2.4-11.19.el5.i386" + "-d0 -e0 -y install cups-1.2.4-11.19.el5.i386" ) @provider.install_package("cups", "1.2.4-11.19.el5") end @@ -479,7 +566,7 @@ describe Chef::Provider::Package::Yum do allow(@new_resource).to receive(:options).and_return("--disablerepo epmd") allow(Chef::Provider::Package::Yum::RPMUtils).to receive(:rpmvercmp).and_return(-1) expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y --disablerepo epmd install cups-11" + "-d0 -e0 -y --disablerepo epmd install cups-11" ) @provider.install_package(@new_resource.name, @provider.candidate_version) end @@ -496,6 +583,7 @@ describe Chef::Provider::Package::Yum do :disable_extra_repo_control => true ) allow(Chef::Provider::Package::Yum::YumCache).to receive(:instance).and_return(@yum_cache) + allow(@yum_cache).to receive(:yum_binary=).with("yum") @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) expect { @provider.install_package("lolcats", "0.99") }.to raise_error(Chef::Exceptions::Package, %r{Version .* not found}) end @@ -514,6 +602,7 @@ describe Chef::Provider::Package::Yum do :disable_extra_repo_control => true ) allow(Chef::Provider::Package::Yum::YumCache).to receive(:instance).and_return(@yum_cache) + allow(@yum_cache).to receive(:yum_binary=).with("yum") @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) @provider.load_current_resource expect { @provider.install_package("cups", "1.2.4-11.15.el5") }.to raise_error(Chef::Exceptions::Package, %r{is newer than candidate package}) @@ -533,10 +622,11 @@ describe Chef::Provider::Package::Yum do :disable_extra_repo_control => true ) allow(Chef::Provider::Package::Yum::YumCache).to receive(:instance).and_return(@yum_cache) + allow(@yum_cache).to receive(:yum_binary=).with("yum") @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) @provider.load_current_resource expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y install cups-1.2.4-11.15.el5" + "-d0 -e0 -y install cups-1.2.4-11.15.el5" ) @provider.install_package("cups", "1.2.4-11.15.el5") end @@ -556,10 +646,11 @@ describe Chef::Provider::Package::Yum do :disable_extra_repo_control => true ) allow(Chef::Provider::Package::Yum::YumCache).to receive(:instance).and_return(@yum_cache) + allow(@yum_cache).to receive(:yum_binary=).with("yum") @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) @provider.load_current_resource expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y downgrade cups-1.2.4-11.15.el5" + "-d0 -e0 -y downgrade cups-1.2.4-11.15.el5" ) @provider.install_package("cups", "1.2.4-11.15.el5") end @@ -569,7 +660,7 @@ describe Chef::Provider::Package::Yum do @provider.load_current_resource allow(Chef::Provider::Package::Yum::RPMUtils).to receive(:rpmvercmp).and_return(-1) expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y install cups-1.2.4-11.15.el5" + "-d0 -e0 -y install cups-1.2.4-11.15.el5" ) expect(@yum_cache).to receive(:reload).once @provider.install_package("cups", "1.2.4-11.15.el5") @@ -580,7 +671,7 @@ describe Chef::Provider::Package::Yum do @provider.load_current_resource allow(Chef::Provider::Package::Yum::RPMUtils).to receive(:rpmvercmp).and_return(-1) expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y install cups-1.2.4-11.15.el5" + "-d0 -e0 -y install cups-1.2.4-11.15.el5" ) expect(@yum_cache).not_to receive(:reload) @provider.install_package("cups", "1.2.4-11.15.el5") @@ -593,7 +684,7 @@ describe Chef::Provider::Package::Yum do allow(@provider).to receive(:candidate_version).and_return('11') allow(Chef::Provider::Package::Yum::RPMUtils).to receive(:rpmvercmp).and_return(-1) expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y install cups-11" + "-d0 -e0 -y install cups-11" ) @provider.upgrade_package(@new_resource.name, @provider.candidate_version) end @@ -604,7 +695,7 @@ describe Chef::Provider::Package::Yum do allow(@provider).to receive(:candidate_version).and_return('11') allow(Chef::Provider::Package::Yum::RPMUtils).to receive(:rpmvercmp).and_return(-1) expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y install cups-11" + "-d0 -e0 -y install cups-11" ) @provider.upgrade_package(@new_resource.name, @provider.candidate_version) end @@ -622,6 +713,7 @@ describe Chef::Provider::Package::Yum do :disable_extra_repo_control => true ) allow(Chef::Provider::Package::Yum::YumCache).to receive(:instance).and_return(@yum_cache) + allow(@yum_cache).to receive(:yum_binary=).with("yum") @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) @provider.load_current_resource expect { @provider.upgrade_package("cups", "1.2.4-11.15.el5") }.to raise_error(Chef::Exceptions::Package, %r{is newer than candidate package}) @@ -671,7 +763,7 @@ describe Chef::Provider::Package::Yum do describe "when removing a package" do it "should run yum remove with the package name" do expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y remove emacs-1.0" + "-d0 -e0 -y remove emacs-1.0" ) @provider.remove_package("emacs", "1.0") end @@ -679,7 +771,7 @@ describe Chef::Provider::Package::Yum do it "should run yum remove with the package name and arch" do allow(@new_resource).to receive(:arch).and_return("x86_64") expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y remove emacs-1.0.x86_64" + "-d0 -e0 -y remove emacs-1.0.x86_64" ) @provider.remove_package("emacs", "1.0") end @@ -688,7 +780,7 @@ describe Chef::Provider::Package::Yum do describe "when purging a package" do it "should run yum remove with the package name" do expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y remove emacs-1.0" + "-d0 -e0 -y remove emacs-1.0" ) @provider.purge_package("emacs", "1.0") end @@ -702,7 +794,7 @@ describe Chef::Provider::Package::Yum do "yum -d0 -e0 -y install emacs-1.0", {:timeout => Chef::Config[:yum_timeout]} ) - @provider.yum_command("yum -d0 -e0 -y install emacs-1.0") + @provider.yum_command("-d0 -e0 -y install emacs-1.0") end it "should run yum once if it exits with a return code > 0 and no scriptlet failures" do @@ -712,7 +804,7 @@ describe Chef::Provider::Package::Yum do "yum -d0 -e0 -y install emacs-1.0", {:timeout => Chef::Config[:yum_timeout]} ) - expect { @provider.yum_command("yum -d0 -e0 -y install emacs-1.0") }.to raise_error(Chef::Exceptions::Exec) + expect { @provider.yum_command("-d0 -e0 -y install emacs-1.0") }.to raise_error(Chef::Exceptions::Exec) end it "should run yum once if it exits with a return code of 1 and %pre scriptlet failures" do @@ -724,7 +816,7 @@ describe Chef::Provider::Package::Yum do {:timeout => Chef::Config[:yum_timeout]} ) # will still raise an exception, can't stub out the subsequent call - expect { @provider.yum_command("yum -d0 -e0 -y install emacs-1.0") }.to raise_error(Chef::Exceptions::Exec) + expect { @provider.yum_command("-d0 -e0 -y install emacs-1.0") }.to raise_error(Chef::Exceptions::Exec) end it "should run yum twice if it exits with a return code of 1 and %post scriptlet failures" do @@ -736,7 +828,20 @@ describe Chef::Provider::Package::Yum do {:timeout => Chef::Config[:yum_timeout]} ) # will still raise an exception, can't stub out the subsequent call - expect { @provider.yum_command("yum -d0 -e0 -y install emacs-1.0") }.to raise_error(Chef::Exceptions::Exec) + expect { @provider.yum_command("-d0 -e0 -y install emacs-1.0") }.to raise_error(Chef::Exceptions::Exec) + end + + it "should pass the yum_binary to the command if its specified" do + @new_resource.yum_binary "yum-deprecated" + expect(@yum_cache).to receive(:yum_binary=).with("yum-deprecated") + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + @status = double("Status", :exitstatus => 0, :stdout => "", :stderr => "") + allow(@provider).to receive(:shell_out).and_return(@status) + expect(@provider).to receive(:shell_out).once.with( + "yum-deprecated -d0 -e0 -y install emacs-1.0", + {:timeout => Chef::Config[:yum_timeout]} + ) + @provider.yum_command("-d0 -e0 -y install emacs-1.0") end end end @@ -1645,6 +1750,14 @@ describe Chef::Provider::Package::Yum::YumCache do end end + let(:yum_exe) { + StringIO.new("#!/usr/bin/python\n\naldsjfa\ldsajflkdsjf\lajsdfj") + } + + let(:bin_exe) { + StringIO.new(SecureRandom.random_bytes) + } + before(:each) do @stdin = double("STDIN", :nil_object => true) @stdout = double("STDOUT", :nil_object => true) @@ -1690,12 +1803,20 @@ file: file://///etc/yum.repos.d/CentOS-Base.repo, line: 12 'qeqwewe\n' EOF @status = double("Status", :exitstatus => 0, :stdin => @stdin, :stdout => @stdout_good, :stderr => @stderr) - # new singleton each time Chef::Provider::Package::Yum::YumCache.reset_instance @yc = Chef::Provider::Package::Yum::YumCache.instance # load valid data + @yc.yum_binary = "yum" allow(@yc).to receive(:shell_out!).and_return(@status) + allow_any_instance_of(described_class).to receive(:which).with("yum").and_return("/usr/bin/yum") + allow(::File).to receive(:open).with("/usr/bin/yum", "r") do |&block| + res = block.call(yum_exe) + # a bit of a hack. rewind this since it seem that no matter what + # I do, we get the same StringIO objects on multiple calls to + # ::File.open + yum_exe.rewind; res + end end describe "initialize" do @@ -1712,6 +1833,24 @@ EOF end end + describe "python_bin" do + it "should return the default python if an error occurs" do + allow(::File).to receive(:open).with("/usr/bin/yum", "r").and_raise(StandardError) + expect(@yc.python_bin).to eq("/usr/bin/python") + end + + it "should return the default python if the yum-executable doesn't start with #!" do + allow(::File).to receive(:open).with("/usr/bin/yum", "r") { |&b| r = b.call(bin_exe); bin_exe.rewind; r} + expect(@yc.python_bin).to eq("/usr/bin/python") + end + + it "should return the interpreter for yum" do + other = StringIO.new("#!/usr/bin/super_python\n\nlasjdfdsaljf\nlasdjfs") + allow(::File).to receive(:open).with("/usr/bin/yum", "r") { |&b| r = b.call(other); other.rewind; r} + expect(@yc.python_bin).to eq("/usr/bin/super_python") + end + end + describe "refresh" do it "should implicitly call yum-dump.py only once by default after being instantiated" do expect(@yc).to receive(:shell_out!).once @@ -1985,6 +2124,8 @@ describe "Chef::Provider::Package::Yum - Multi" do :disable_extra_repo_control => true ) allow(Chef::Provider::Package::Yum::YumCache).to receive(:instance).and_return(@yum_cache) + allow(@yum_cache).to receive(:yum_binary=).with("yum") + allow(@yum_cache).to receive(:yum_binary=).with("yum") @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) @pid = double("PID") end @@ -2027,6 +2168,36 @@ describe "Chef::Provider::Package::Yum - Multi" do it "should return the current resouce" do expect(@provider.load_current_resource).to eql(@provider.current_resource) end + + describe "when version constraint in package_name" do + it "should set package_version if no existing package_name is found and new_package_name is available" do + @new_resource = Chef::Resource::Package.new(['cups = 1.2.4-11.18.el5_2.3', 'emacs = 24.4']) + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + allow(@yum_cache).to receive(:package_available?) { |pkg| %w(cups emacs).include?(pkg) ? true : false } + allow(@yum_cache).to receive(:candidate_version) do |pkg| + if pkg == 'cups' + "1.2.4-11.18.el5_2.3" + elsif pkg == 'emacs' + "24.4" + end + end + allow(@yum_cache).to receive(:packages_from_require) do |pkg| + if pkg.name == 'cups' + [Chef::Provider::Package::Yum::RPMDbPackage.new("cups", "1.2.4-11.18.el5_2.3", "noarch", [], false, true, "base")] + elsif pkg.name == 'emacs' + [Chef::Provider::Package::Yum::RPMDbPackage.new("emacs", "24.4", "noarch", [], false, true, "base")] + end + end + expect(Chef::Log).to receive(:debug).exactly(2).times.with(%r{matched 1 package,}) + expect(Chef::Log).to receive(:debug).exactly(1).times.with(%r{candidate version: \["1.2.4-11.18.el5_2.3", "24.4"\]}) + expect(Chef::Log).to receive(:debug).at_least(2).times.with(%r{checking yum info}) + @provider.load_current_resource + expect(@provider.new_resource.package_name).to eq(["cups", "emacs"]) + expect(@provider.new_resource.version).to eq(["1.2.4-11.18.el5_2.3", "24.4"]) + expect(@provider.send(:package_name_array)).to eq(["cups", "emacs"]) + expect(@provider.send(:new_version_array)).to eq(["1.2.4-11.18.el5_2.3", "24.4"]) + end + end end describe "when installing a package" do @@ -2036,7 +2207,7 @@ describe "Chef::Provider::Package::Yum - Multi" do allow(@yum_cache).to receive(:installed_version).with('cups', nil).and_return('1.2.4-11.18.el5') allow(@yum_cache).to receive(:installed_version).with('vim', nil).and_return('0.9') expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y install cups-1.2.4-11.19.el5 vim-1.0" + "-d0 -e0 -y install cups-1.2.4-11.19.el5 vim-1.0" ) @provider.install_package(["cups", "vim"], ["1.2.4-11.19.el5", '1.0']) end @@ -2046,7 +2217,7 @@ describe "Chef::Provider::Package::Yum - Multi" do allow(@new_resource).to receive(:arch).and_return("i386") allow(Chef::Provider::Package::Yum::RPMUtils).to receive(:rpmvercmp).and_return(-1) expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y install cups-1.2.4-11.19.el5.i386 vim-1.0.i386" + "-d0 -e0 -y install cups-1.2.4-11.19.el5.i386 vim-1.0.i386" ) @provider.install_package(["cups", "vim"], ["1.2.4-11.19.el5", "1.0"]) end @@ -2057,10 +2228,36 @@ describe "Chef::Provider::Package::Yum - Multi" do allow(@yum_cache).to receive(:installed_version).with('cups', nil).and_return('1.2.4-11.18.el5') allow(@yum_cache).to receive(:installed_version).with('vim', nil).and_return('0.9') expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y --disablerepo epmd install cups-1.2.4-11.19.el5 vim-1.0" + "-d0 -e0 -y --disablerepo epmd install cups-1.2.4-11.19.el5 vim-1.0" ) allow(@new_resource).to receive(:options).and_return("--disablerepo epmd") @provider.install_package(["cups", "vim"], ["1.2.4-11.19.el5", '1.0']) end + + it "should run yum install with the package name and version when name has arch" do + @new_resource = Chef::Resource::Package.new(['cups.x86_64', 'vim']) + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + allow(Chef::Provider::Package::Yum::RPMUtils).to receive(:rpmvercmp).and_return(-1) + + # Inside of load_current_resource() we'll call parse_arch for cups, + # and we need to craft the right response. The default mock setup above + # will just return valid versions all the time which won't work for this + # test. + allow(@yum_cache).to receive(:installed_version).with('cups', 'x86_64').and_return('XXXX') + allow(@yum_cache).to receive(:candidate_version).with('cups', 'x86_64').and_return('1.2.4-11.18.el5') + allow(@yum_cache).to receive(:installed_version).with('cups.x86_64').and_return(nil) + allow(@yum_cache).to receive(:candidate_version).with('cups.x86_64').and_return(nil) + + # Normal mock's for the idempotency check + allow(@yum_cache).to receive(:installed_version).with('cups', nil).and_return('1.2.4-11.18.el5') + allow(@yum_cache).to receive(:installed_version).with('vim', nil).and_return('0.9') + + @provider.load_current_resource + expect(@provider).to receive(:yum_command).with( + "-d0 -e0 -y install cups-1.2.4-11.19.el5.x86_64 vim-1.0" + ) + @provider.install_package(["cups", "vim"], ["1.2.4-11.19.el5", '1.0']) + end + end end diff --git a/spec/unit/provider/package/zypper_spec.rb b/spec/unit/provider/package/zypper_spec.rb index 706ad722dd..18ff739bc6 100644 --- a/spec/unit/provider/package/zypper_spec.rb +++ b/spec/unit/provider/package/zypper_spec.rb @@ -19,126 +19,150 @@ require 'spec_helper' describe Chef::Provider::Package::Zypper do + let!(:new_resource) { Chef::Resource::ZypperPackage.new("cups") } + + let!(:current_resource) { Chef::Resource::ZypperPackage.new("cups") } + + let(:provider) do + node = Chef::Node.new + events = Chef::EventDispatch::Dispatcher.new + run_context = Chef::RunContext.new(node, {}, events) + Chef::Provider::Package::Zypper.new(new_resource, run_context) + end + + let(:status) { double(:stdout => "\n", :exitstatus => 0) } + before(:each) do - @node = Chef::Node.new - @events = Chef::EventDispatch::Dispatcher.new - @run_context = Chef::RunContext.new(@node, {}, @events) - @new_resource = Chef::Resource::Package.new("cups") - - @current_resource = Chef::Resource::Package.new("cups") - - @provider = Chef::Provider::Package::Zypper.new(@new_resource, @run_context) - allow(Chef::Resource::Package).to receive(:new).and_return(@current_resource) - @status = double(:stdout => "\n", :exitstatus => 0) - allow(@provider).to receive(:shell_out).and_return(@status) - allow(@provider).to receive(:`).and_return("2.0") + allow(Chef::Resource::Package).to receive(:new).and_return(current_resource) + allow(provider).to receive(:shell_out).and_return(status) + allow(provider).to receive(:`).and_return("2.0") + end + + def shell_out_expectation(command, options=nil) + options ||= { timeout: 900 } + expect(provider).to receive(:shell_out).with(command, options) + end + + def shell_out_expectation!(command, options=nil) + options ||= { timeout: 900 } + expect(provider).to receive(:shell_out!).with(command, options) end describe "when loading the current package state" do it "should create a current resource with the name of the new_resource" do - expect(Chef::Resource::Package).to receive(:new).and_return(@current_resource) - @provider.load_current_resource + expect(Chef::Resource::Package).to receive(:new).with(new_resource.name).and_return(current_resource) + provider.load_current_resource end it "should set the current resources package name to the new resources package name" do - expect(@current_resource).to receive(:package_name).with(@new_resource.package_name) - @provider.load_current_resource + expect(current_resource).to receive(:package_name).with(new_resource.package_name) + provider.load_current_resource end it "should run zypper info with the package name" do - expect(@provider).to receive(:shell_out).with("zypper --non-interactive info #{@new_resource.package_name}").and_return(@status) - @provider.load_current_resource + shell_out_expectation( + "zypper --non-interactive info #{new_resource.package_name}" + ).and_return(status) + provider.load_current_resource end it "should set the installed version to nil on the current resource if zypper info installed version is (none)" do - allow(@provider).to receive(:shell_out).and_return(@status) - expect(@current_resource).to receive(:version).with(nil).and_return(true) - @provider.load_current_resource + allow(provider).to receive(:shell_out).and_return(status) + expect(current_resource).to receive(:version).with(nil).and_return(true) + provider.load_current_resource end it "should set the installed version if zypper info has one" do status = double(:stdout => "Version: 1.0\nInstalled: Yes\n", :exitstatus => 0) - allow(@provider).to receive(:shell_out).and_return(status) - expect(@current_resource).to receive(:version).with("1.0").and_return(true) - @provider.load_current_resource + allow(provider).to receive(:shell_out).and_return(status) + expect(current_resource).to receive(:version).with("1.0").and_return(true) + provider.load_current_resource end it "should set the candidate version if zypper info has one" do status = double(:stdout => "Version: 1.0\nInstalled: No\nStatus: out-of-date (version 0.9 installed)", :exitstatus => 0) - allow(@provider).to receive(:shell_out).and_return(status) - @provider.load_current_resource - expect(@provider.candidate_version).to eql("1.0") + allow(provider).to receive(:shell_out).and_return(status) + provider.load_current_resource + expect(provider.candidate_version).to eql("1.0") end it "should raise an exception if zypper info fails" do - expect(@status).to receive(:exitstatus).and_return(1) - expect { @provider.load_current_resource }.to raise_error(Chef::Exceptions::Package) + expect(status).to receive(:exitstatus).and_return(1) + expect { provider.load_current_resource }.to raise_error(Chef::Exceptions::Package) end it "should not raise an exception if zypper info succeeds" do - expect(@status).to receive(:exitstatus).and_return(0) - expect { @provider.load_current_resource }.not_to raise_error + expect(status).to receive(:exitstatus).and_return(0) + expect { provider.load_current_resource }.not_to raise_error end it "should return the current resouce" do - expect(@provider.load_current_resource).to eql(@current_resource) + expect(provider.load_current_resource).to eql(current_resource) end end describe "install_package" do it "should run zypper install with the package name and version" do allow(Chef::Config).to receive(:[]).with(:zypper_check_gpg).and_return(true) - expect(@provider).to receive(:shell_out!).with( - "zypper --non-interactive install --auto-agree-with-licenses emacs=1.0") - @provider.install_package("emacs", "1.0") + shell_out_expectation!( + "zypper --non-interactive install --auto-agree-with-licenses emacs=1.0" + ) + provider.install_package("emacs", "1.0") end it "should run zypper install without gpg checks" do allow(Chef::Config).to receive(:[]).with(:zypper_check_gpg).and_return(false) - expect(@provider).to receive(:shell_out!).with( + shell_out_expectation!( "zypper --non-interactive --no-gpg-checks install "+ - "--auto-agree-with-licenses emacs=1.0") - @provider.install_package("emacs", "1.0") + "--auto-agree-with-licenses emacs=1.0" + ) + provider.install_package("emacs", "1.0") end it "should warn about gpg checks on zypper install" do expect(Chef::Log).to receive(:warn).with( - /All packages will be installed without gpg signature checks/) - expect(@provider).to receive(:shell_out!).with( + /All packages will be installed without gpg signature checks/ + ) + shell_out_expectation!( "zypper --non-interactive --no-gpg-checks install "+ - "--auto-agree-with-licenses emacs=1.0") - @provider.install_package("emacs", "1.0") + "--auto-agree-with-licenses emacs=1.0" + ) + provider.install_package("emacs", "1.0") end end describe "upgrade_package" do it "should run zypper update with the package name and version" do allow(Chef::Config).to receive(:[]).with(:zypper_check_gpg).and_return(true) - expect(@provider).to receive(:shell_out!).with( - "zypper --non-interactive install --auto-agree-with-licenses emacs=1.0") - @provider.upgrade_package("emacs", "1.0") + shell_out_expectation!( + "zypper --non-interactive install --auto-agree-with-licenses emacs=1.0" + ) + provider.upgrade_package("emacs", "1.0") end it "should run zypper update without gpg checks" do allow(Chef::Config).to receive(:[]).with(:zypper_check_gpg).and_return(false) - expect(@provider).to receive(:shell_out!).with( + shell_out_expectation!( "zypper --non-interactive --no-gpg-checks install "+ - "--auto-agree-with-licenses emacs=1.0") - @provider.upgrade_package("emacs", "1.0") + "--auto-agree-with-licenses emacs=1.0" + ) + provider.upgrade_package("emacs", "1.0") end it "should warn about gpg checks on zypper upgrade" do expect(Chef::Log).to receive(:warn).with( - /All packages will be installed without gpg signature checks/) - expect(@provider).to receive(:shell_out!).with( + /All packages will be installed without gpg signature checks/ + ) + shell_out_expectation!( "zypper --non-interactive --no-gpg-checks install "+ - "--auto-agree-with-licenses emacs=1.0") - @provider.upgrade_package("emacs", "1.0") + "--auto-agree-with-licenses emacs=1.0" + ) + provider.upgrade_package("emacs", "1.0") end it "should run zypper upgrade without gpg checks" do - expect(@provider).to receive(:shell_out!).with( + shell_out_expectation!( "zypper --non-interactive --no-gpg-checks install "+ - "--auto-agree-with-licenses emacs=1.0") - - @provider.upgrade_package("emacs", "1.0") + "--auto-agree-with-licenses emacs=1.0" + ) + provider.upgrade_package("emacs", "1.0") end end @@ -147,83 +171,94 @@ describe Chef::Provider::Package::Zypper do context "when package version is not explicitly specified" do it "should run zypper remove with the package name" do allow(Chef::Config).to receive(:[]).with(:zypper_check_gpg).and_return(true) - expect(@provider).to receive(:shell_out!).with( - "zypper --non-interactive remove emacs") - @provider.remove_package("emacs", nil) + shell_out_expectation!( + "zypper --non-interactive remove emacs" + ) + provider.remove_package("emacs", nil) end end context "when package version is explicitly specified" do it "should run zypper remove with the package name" do allow(Chef::Config).to receive(:[]).with(:zypper_check_gpg).and_return(true) - expect(@provider).to receive(:shell_out!).with( - "zypper --non-interactive remove emacs=1.0") - @provider.remove_package("emacs", "1.0") + shell_out_expectation!( + "zypper --non-interactive remove emacs=1.0" + ) + provider.remove_package("emacs", "1.0") end it "should run zypper remove without gpg checks" do allow(Chef::Config).to receive(:[]).with(:zypper_check_gpg).and_return(false) - expect(@provider).to receive(:shell_out!).with( - "zypper --non-interactive --no-gpg-checks remove emacs=1.0") - @provider.remove_package("emacs", "1.0") + shell_out_expectation!( + "zypper --non-interactive --no-gpg-checks remove emacs=1.0" + ) + provider.remove_package("emacs", "1.0") end it "should warn about gpg checks on zypper remove" do expect(Chef::Log).to receive(:warn).with( - /All packages will be installed without gpg signature checks/) - expect(@provider).to receive(:shell_out!).with( - "zypper --non-interactive --no-gpg-checks remove emacs=1.0") - - @provider.remove_package("emacs", "1.0") + /All packages will be installed without gpg signature checks/ + ) + shell_out_expectation!( + "zypper --non-interactive --no-gpg-checks remove emacs=1.0" + ) + provider.remove_package("emacs", "1.0") end end end describe "purge_package" do it "should run remove_package with the name and version" do - expect(@provider).to receive(:shell_out!).with( - "zypper --non-interactive --no-gpg-checks remove --clean-deps emacs=1.0") - @provider.purge_package("emacs", "1.0") + shell_out_expectation!( + "zypper --non-interactive --no-gpg-checks remove --clean-deps emacs=1.0" + ) + provider.purge_package("emacs", "1.0") end it "should run zypper purge without gpg checks" do allow(Chef::Config).to receive(:[]).with(:zypper_check_gpg).and_return(false) - expect(@provider).to receive(:shell_out!).with( - "zypper --non-interactive --no-gpg-checks remove --clean-deps emacs=1.0") - @provider.purge_package("emacs", "1.0") + shell_out_expectation!( + "zypper --non-interactive --no-gpg-checks remove --clean-deps emacs=1.0" + ) + provider.purge_package("emacs", "1.0") end it "should warn about gpg checks on zypper purge" do expect(Chef::Log).to receive(:warn).with( - /All packages will be installed without gpg signature checks/) - expect(@provider).to receive(:shell_out!).with( - "zypper --non-interactive --no-gpg-checks remove --clean-deps emacs=1.0") - @provider.purge_package("emacs", "1.0") + /All packages will be installed without gpg signature checks/ + ) + shell_out_expectation!( + "zypper --non-interactive --no-gpg-checks remove --clean-deps emacs=1.0" + ) + provider.purge_package("emacs", "1.0") end end describe "on an older zypper" do before(:each) do - allow(@provider).to receive(:`).and_return("0.11.6") + allow(provider).to receive(:`).and_return("0.11.6") end describe "install_package" do it "should run zypper install with the package name and version" do - expect(@provider).to receive(:shell_out!).with( - "zypper --no-gpg-checks install --auto-agree-with-licenses -y emacs") - @provider.install_package("emacs", "1.0") + shell_out_expectation!( + "zypper --no-gpg-checks install --auto-agree-with-licenses -y emacs" + ) + provider.install_package("emacs", "1.0") end end describe "upgrade_package" do it "should run zypper update with the package name and version" do - expect(@provider).to receive(:shell_out!).with( - "zypper --no-gpg-checks install --auto-agree-with-licenses -y emacs") - @provider.upgrade_package("emacs", "1.0") + shell_out_expectation!( + "zypper --no-gpg-checks install --auto-agree-with-licenses -y emacs" + ) + provider.upgrade_package("emacs", "1.0") end end describe "remove_package" do it "should run zypper remove with the package name" do - expect(@provider).to receive(:shell_out!).with( - "zypper --no-gpg-checks remove -y emacs") - @provider.remove_package("emacs", "1.0") + shell_out_expectation!( + "zypper --no-gpg-checks remove -y emacs" + ) + provider.remove_package("emacs", "1.0") end end end diff --git a/spec/unit/provider/package_spec.rb b/spec/unit/provider/package_spec.rb index 1633d18f9d..432d968906 100644 --- a/spec/unit/provider/package_spec.rb +++ b/spec/unit/provider/package_spec.rb @@ -37,6 +37,12 @@ describe Chef::Provider::Package do allow(@provider).to receive(:install_package).and_return(true) end + it "raises a Chef::Exceptions::InvalidResourceSpecification if both multipackage and source are provided" do + @new_resource.package_name(['a', 'b']) + @new_resource.source('foo') + expect { @provider.run_action(:install) }.to raise_error(Chef::Exceptions::InvalidResourceSpecification) + end + it "should raise a Chef::Exceptions::Package if no version is specified, and no candidate is available" do @provider.candidate_version = nil expect { @provider.run_action(:install) }.to raise_error(Chef::Exceptions::Package) @@ -698,4 +704,38 @@ describe "Chef::Provider::Package - Multi" do expect(@new_resource).not_to be_updated_by_last_action end end + + describe "shell_out helpers" do + [ :shell_out_with_timeout, :shell_out_with_timeout! ].each do |method| + stubbed_method = method == :shell_out_with_timeout! ? :shell_out! : :shell_out + [ %w{command arg1 arg2}, "command arg1 arg2" ].each do |command| + it "#{method} defaults to 900 seconds" do + expect(@provider).to receive(stubbed_method).with(*command, timeout: 900) + @provider.send(method, *command) + end + it "#{method} overrides the default timeout with its options" do + expect(@provider).to receive(stubbed_method).with(*command, timeout: 1) + @provider.send(method, *command, timeout: 1) + end + it "#{method} overrides both timeouts with the new_resource.timeout" do + @new_resource.timeout(99) + expect(@provider).to receive(stubbed_method).with(*command, timeout: 99) + @provider.send(method, *command, timeout: 1) + end + it "#{method} defaults to 900 seconds and preserves options" do + expect(@provider).to receive(stubbed_method).with(*command, env: nil, timeout: 900) + @provider.send(method, *command, env: nil) + end + it "#{method} overrides the default timeout with its options and preserves options" do + expect(@provider).to receive(stubbed_method).with(*command, timeout: 1, env: nil) + @provider.send(method, *command, timeout: 1, env: nil) + end + it "#{method} overrides both timeouts with the new_resource.timeout and preseves options" do + @new_resource.timeout(99) + expect(@provider).to receive(stubbed_method).with(*command, timeout: 99, env: nil) + @provider.send(method, *command, timeout: 1, env: nil) + end + end + end + end end diff --git a/spec/unit/provider/powershell_script_spec.rb b/spec/unit/provider/powershell_script_spec.rb new file mode 100644 index 0000000000..855c18af9b --- /dev/null +++ b/spec/unit/provider/powershell_script_spec.rb @@ -0,0 +1,80 @@ +# +# Author:: Adam Edwards (<adamed@opscode.com>) +# Copyright:: Copyright (c) 2013 Opscode, 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::PowershellScript, "action_run" do + + let(:powershell_version) { nil } + let(:node) { + node = Chef::Node.new + node.default["kernel"] = Hash.new + node.default["kernel"][:machine] = :x86_64.to_s + if ! powershell_version.nil? + node.default[:languages] = { :powershell => { :version => powershell_version } } + end + node + } + + let(:provider) { + empty_events = Chef::EventDispatch::Dispatcher.new + run_context = Chef::RunContext.new(node, {}, empty_events) + new_resource = Chef::Resource::PowershellScript.new('run some powershell code', run_context) + Chef::Provider::PowershellScript.new(new_resource, run_context) + } + + context 'when setting interpreter flags' do + it "should set the -File flag as the last flag" do + expect(provider.flags.split(' ').pop).to eq("-File") + end + + let(:execution_policy_flag) do + execution_policy_index = 0 + provider_flags = provider.flags.split(' ') + execution_policy_specified = false + + provider_flags.find do | value | + execution_policy_index += 1 + execution_policy_specified = value.downcase == '-ExecutionPolicy'.downcase + end + + execution_policy = execution_policy_specified ? provider_flags[execution_policy_index] : nil + end + + context 'when running with an unspecified PowerShell version' do + let(:powershell_version) { nil } + it "should set the -ExecutionPolicy flag to 'Unrestricted' by default" do + expect(execution_policy_flag.downcase).to eq('unrestricted'.downcase) + end + end + + { '2.0' => 'Unrestricted', + '2.5' => 'Unrestricted', + '3.0' => 'Bypass', + '3.6' => 'Bypass', + '4.0' => 'Bypass', + '5.0' => 'Bypass' }.each do | version_policy | + let(:powershell_version) { version_policy[0].to_f } + context "when running PowerShell version #{version_policy[0]}" do + let(:powershell_version) { version_policy[0].to_f } + it "should set the -ExecutionPolicy flag to '#{version_policy[1]}'" do + expect(execution_policy_flag.downcase).to eq(version_policy[1].downcase) + end + end + end + end +end diff --git a/spec/unit/provider/registry_key_spec.rb b/spec/unit/provider/registry_key_spec.rb index 79811fdab8..47543ffe39 100644 --- a/spec/unit/provider/registry_key_spec.rb +++ b/spec/unit/provider/registry_key_spec.rb @@ -77,6 +77,18 @@ shared_examples_for "a registry key" do end describe "action_create" do + context "when a case insensitive match for the key exists" do + before(:each) do + expect(@double_registry).to receive(:key_exists?).twice.with(keyname.downcase).and_return(true) + end + it "should do nothing if the if a case insensitive key and the value both exist" do + @provider.new_resource.key(keyname.downcase) + expect(@double_registry).to receive(:get_values).with(keyname.downcase).and_return( testval1 ) + expect(@double_registry).not_to receive(:set_value) + @provider.load_current_resource + @provider.action_create + end + end context "when the key exists" do before(:each) do expect(@double_registry).to receive(:key_exists?).twice.with(keyname).and_return(true) diff --git a/spec/unit/provider/remote_directory_spec.rb b/spec/unit/provider/remote_directory_spec.rb index 4434714ebc..99e2fe285c 100644 --- a/spec/unit/provider/remote_directory_spec.rb +++ b/spec/unit/provider/remote_directory_spec.rb @@ -194,8 +194,8 @@ describe Chef::Provider::RemoteDirectory do expect(::File.exist?(symlinked_dir_path)).to be_falsey expect(::File.exist?(tmp_dir)).to be_truthy - rescue Chef::Exceptions::Win32APIError => e - pending "This must be run as an Administrator to create symlinks" + rescue Chef::Exceptions::Win32APIError + skip "This must be run as an Administrator to create symlinks" end end end diff --git a/spec/unit/provider/remote_file/fetcher_spec.rb b/spec/unit/provider/remote_file/fetcher_spec.rb index c049848fbf..8bd3b7c625 100644 --- a/spec/unit/provider/remote_file/fetcher_spec.rb +++ b/spec/unit/provider/remote_file/fetcher_spec.rb @@ -24,6 +24,26 @@ describe Chef::Provider::RemoteFile::Fetcher do let(:new_resource) { double("new resource") } let(:fetcher_instance) { double("fetcher") } + describe "when passed a network share" do + before do + expect(Chef::Provider::RemoteFile::NetworkFile).to receive(:new).and_return(fetcher_instance) + end + + context "when host is a name" do + let(:source) { "\\\\foohost\\fooshare\\Foo.tar.gz" } + it "returns a network file fetcher" do + expect(described_class.for_resource(source, new_resource, current_resource)).to eq(fetcher_instance) + end + end + + context "when host is an ip" do + let(:source) { "\\\\127.0.0.1\\fooshare\\Foo.tar.gz" } + it "returns a network file fetcher" do + expect(described_class.for_resource(source, new_resource, current_resource)).to eq(fetcher_instance) + end + end + end + describe "when passed an http url" do let(:uri) { double("uri", :scheme => "http" ) } before do @@ -72,4 +92,3 @@ describe Chef::Provider::RemoteFile::Fetcher do end end - diff --git a/spec/unit/provider/remote_file/local_file_spec.rb b/spec/unit/provider/remote_file/local_file_spec.rb index b33d82f624..575996a540 100644 --- a/spec/unit/provider/remote_file/local_file_spec.rb +++ b/spec/unit/provider/remote_file/local_file_spec.rb @@ -25,26 +25,45 @@ describe Chef::Provider::RemoteFile::LocalFile do let(:new_resource) { Chef::Resource::RemoteFile.new("local file backend test (new_resource)") } let(:current_resource) { Chef::Resource::RemoteFile.new("local file backend test (current_resource)") } subject(:fetcher) { Chef::Provider::RemoteFile::LocalFile.new(uri, new_resource, current_resource) } - - context "when parsing source path" do + + context "when parsing source path on windows" do + + before do + allow(Chef::Platform).to receive(:windows?).and_return(true) + end + describe "when given local unix path" do let(:uri) { URI.parse("file:///nyan_cat.png") } it "returns a correct unix path" do - expect(fetcher.fix_windows_path(uri.path)).to eq("/nyan_cat.png") + expect(fetcher.source_path).to eq("/nyan_cat.png") end end describe "when given local windows path" do let(:uri) { URI.parse("file:///z:/windows/path/file.txt") } it "returns a valid windows local path" do - expect(fetcher.fix_windows_path(uri.path)).to eq("z:/windows/path/file.txt") + expect(fetcher.source_path).to eq("z:/windows/path/file.txt") + end + end + + describe "when given local windows path with spaces" do + let(:uri) { URI.parse(URI.escape("file:///z:/windows/path/foo & bar.txt")) } + it "returns a valid windows local path" do + expect(fetcher.source_path).to eq("z:/windows/path/foo & bar.txt") end end describe "when given unc windows path" do let(:uri) { URI.parse("file:////server/share/windows/path/file.txt") } it "returns a valid windows unc path" do - expect(fetcher.fix_windows_path(uri.path)).to eq("//server/share/windows/path/file.txt") + expect(fetcher.source_path).to eq("//server/share/windows/path/file.txt") + end + end + + describe "when given unc windows path with spaces" do + let(:uri) { URI.parse(URI.escape("file:////server/share/windows/path/foo & bar.txt")) } + it "returns a valid windows unc path" do + expect(fetcher.source_path).to eq("//server/share/windows/path/foo & bar.txt") end end end @@ -73,7 +92,7 @@ describe Chef::Provider::RemoteFile::LocalFile do it "stages the local file to a temporary file" do expect(Chef::FileContentManagement::Tempfile).to receive(:new).with(new_resource).and_return(chef_tempfile) expect(::FileUtils).to receive(:cp).with(uri.path, tempfile.path) - expect(tempfile).to receive(:close) + expect(tempfile).to receive(:close) result = fetcher.fetch expect(result).to eq(tempfile) diff --git a/spec/unit/provider/remote_file/network_file_spec.rb b/spec/unit/provider/remote_file/network_file_spec.rb new file mode 100644 index 0000000000..3666a47468 --- /dev/null +++ b/spec/unit/provider/remote_file/network_file_spec.rb @@ -0,0 +1,45 @@ +# +# Author:: Jay Mundrawala (<jdm@chef.io>) +# Copyright:: Copyright (c) 2015 Chef Software +# 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::RemoteFile::NetworkFile do + + let(:source) { "\\\\foohost\\fooshare\\Foo.tar.gz" } + + let(:new_resource) { Chef::Resource::RemoteFile.new("network file (new_resource)") } + let(:current_resource) { Chef::Resource::RemoteFile.new("network file (current_resource)") } + subject(:fetcher) { Chef::Provider::RemoteFile::NetworkFile.new(source, new_resource, current_resource) } + + describe "when fetching the object" do + + let(:tempfile) { double("Tempfile", :path => "/tmp/foo/bar/Foo.tar.gz", :close => nil) } + let(:chef_tempfile) { double("Chef::FileContentManagement::Tempfile", :tempfile => tempfile) } + + it "stages the local file to a temporary file" do + expect(Chef::FileContentManagement::Tempfile).to receive(:new).with(new_resource).and_return(chef_tempfile) + expect(::FileUtils).to receive(:cp).with(source, tempfile.path) + expect(tempfile).to receive(:close) + + result = fetcher.fetch + expect(result).to eq(tempfile) + end + + end + +end diff --git a/spec/unit/provider/service/aix_service_spec.rb b/spec/unit/provider/service/aix_service_spec.rb index 796661145b..5cca7d6f0a 100644 --- a/spec/unit/provider/service/aix_service_spec.rb +++ b/spec/unit/provider/service/aix_service_spec.rb @@ -51,22 +51,35 @@ describe Chef::Provider::Service::Aix do end it "current resource is running" do - expect(@provider).to receive(:shell_out!).with("lssrc -a | grep -w chef").and_return(@status) - expect(@provider).to receive(:is_resource_group?).with(["chef chef 12345 active"]) + expect(@provider).to receive(:shell_out!).with("lssrc -s chef").and_return(@status) + expect(@provider).to receive(:is_resource_group?).and_return false @provider.load_current_resource expect(@current_resource.running).to be_truthy end end - context "when the service is inoprative" do + context "when the service is inoperative" do before do @status = double("Status", :exitstatus => 0, :stdout => "chef chef inoperative\n") end it "current resource is not running" do - expect(@provider).to receive(:shell_out!).with("lssrc -a | grep -w chef").and_return(@status) - expect(@provider).to receive(:is_resource_group?).with(["chef chef inoperative"]) + expect(@provider).to receive(:shell_out!).with("lssrc -s chef").and_return(@status) + expect(@provider).to receive(:is_resource_group?).and_return false + + @provider.load_current_resource + expect(@current_resource.running).to be_falsey + end + end + + context "when there is no such service" do + before do + @status = double("Status", :exitstatus => 1, :stdout => "0513-085 The chef Subsystem is not on file.\n") + end + it "current resource is not running" do + expect(@provider).to receive(:shell_out!).with("lssrc -s chef").and_return(@status) + expect(@provider).to receive(:is_resource_group?).and_return false @provider.load_current_resource expect(@current_resource.running).to be_falsey @@ -75,13 +88,13 @@ describe Chef::Provider::Service::Aix do end describe "is resource group" do - context "when there are mutiple subsystems associated with group" do + context "when there are multiple subsystems associated with group" do before do @status = double("Status", :exitstatus => 0, :stdout => "chef1 chef 12345 active\nchef2 chef 12334 active\nchef3 chef inoperative") end it "service is a group" do - expect(@provider).to receive(:shell_out!).with("lssrc -a | grep -w chef").and_return(@status) + expect(@provider).to receive(:shell_out).with("lssrc -g chef").and_return(@status) @provider.load_current_resource expect(@provider.instance_eval("@is_resource_group")).to be_truthy end @@ -93,19 +106,21 @@ describe Chef::Provider::Service::Aix do end it "service is a group" do - expect(@provider).to receive(:shell_out!).with("lssrc -a | grep -w chef").and_return(@status) + expect(@provider).to receive(:shell_out).with("lssrc -g chef").and_return(@status) @provider.load_current_resource expect(@provider.instance_eval("@is_resource_group")).to be_truthy end end - context "when there service is a subsytem" do + context "when the service is a subsystem" do before do - @status = double("Status", :exitstatus => 0, :stdout => "chef chef123 inoperative\n") + @group_status = double("Status", :exitstatus => 1, :stdout => "0513-086 The chef Group is not on file.\n") + @service_status = double("Status", :exitstatus => 0, :stdout => "chef chef inoperative\n") end it "service is a subsystem" do - expect(@provider).to receive(:shell_out!).with("lssrc -a | grep -w chef").and_return(@status) + expect(@provider).to receive(:shell_out).with("lssrc -g chef").and_return(@group_status) + expect(@provider).to receive(:shell_out!).with("lssrc -s chef").and_return(@service_status) @provider.load_current_resource expect(@provider.instance_eval("@is_resource_group")).to be_falsey end diff --git a/spec/unit/provider/service/freebsd_service_spec.rb b/spec/unit/provider/service/freebsd_service_spec.rb index 5a55425d87..cfc28c94d5 100644 --- a/spec/unit/provider/service/freebsd_service_spec.rb +++ b/spec/unit/provider/service/freebsd_service_spec.rb @@ -189,18 +189,6 @@ PS_SAMPLE expect(provider.status_load_success).to be_nil end - context "when ps command is nil" do - before do - node.automatic_attrs[:command] = {:ps => nil} - end - - it "should set running to nil" do - pending "superclass raises no conversion of nil to string which seems broken" - provider.determine_current_status! - expect(current_resource.running).to be_nil - end - end - context "when ps is empty string" do before do node.automatic_attrs[:command] = {:ps => ""} diff --git a/spec/unit/provider/service/gentoo_service_spec.rb b/spec/unit/provider/service/gentoo_service_spec.rb index c08982acc3..0aa7bf4529 100644 --- a/spec/unit/provider/service/gentoo_service_spec.rb +++ b/spec/unit/provider/service/gentoo_service_spec.rb @@ -1,7 +1,7 @@ # # Author:: Lee Jensen (<ljensen@engineyard.com>) # Author:: AJ Christensen (<aj@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Copyright:: Copyright (c) 2008-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -108,17 +108,17 @@ describe Chef::Provider::Service::Gentoo do it "should support the status command automatically" do @provider.load_current_resource - expect(@new_resource.supports[:status]).to be_truthy + expect(@provider.supports[:status]).to be true end it "should support the restart command automatically" do @provider.load_current_resource - expect(@new_resource.supports[:restart]).to be_truthy + expect(@provider.supports[:restart]).to be true end it "should not support the reload command automatically" do @provider.load_current_resource - expect(@new_resource.supports[:reload]).not_to be_truthy + expect(@provider.supports[:reload]).to be_falsey end end diff --git a/spec/unit/provider/service/macosx_spec.rb b/spec/unit/provider/service/macosx_spec.rb index fb751592df..54183bdc3d 100644 --- a/spec/unit/provider/service/macosx_spec.rb +++ b/spec/unit/provider/service/macosx_spec.rb @@ -22,17 +22,17 @@ describe Chef::Provider::Service::Macosx do describe ".gather_plist_dirs" do context "when HOME directory is set" do before do - allow(ENV).to receive(:[]).with('HOME').and_return("/User/someuser") + allow(Chef::Util::PathHelper).to receive(:home).with('Library', 'LaunchAgents').and_yield('/Users/someuser/Library/LaunchAgents') end it "includes users's LaunchAgents folder" do - expect(described_class.gather_plist_dirs).to include("#{ENV['HOME']}/Library/LaunchAgents") + expect(described_class.gather_plist_dirs).to include("/Users/someuser/Library/LaunchAgents") end end context "when HOME directory is not set" do before do - allow(ENV).to receive(:[]).with('HOME').and_return(nil) + allow(Chef::Util::PathHelper).to receive(:home).with('Library', 'LaunchAgents').and_return(nil) end it "doesn't include user's LaunchAgents folder" do @@ -58,248 +58,275 @@ describe Chef::Provider::Service::Macosx do </plist> XML - ["redis-server", "io.redis.redis-server"].each do |service_name| - before do - allow(Dir).to receive(:glob).and_return(["/Users/igor/Library/LaunchAgents/io.redis.redis-server.plist"], []) - allow(provider).to receive(:shell_out!). - with("launchctl list", {:group => 1001, :user => 101}). - and_return(double("Status", :stdout => launchctl_stdout)) - allow(provider).to receive(:shell_out). - with(/launchctl list /, - {:group => nil, :user => nil}). - and_return(double("Status", - :stdout => launchctl_stdout, :exitstatus => 0)) - allow(provider).to receive(:shell_out!). - with(/plutil -convert xml1 -o/). - and_return(double("Status", :stdout => plutil_stdout)) - - allow(File).to receive(:stat).and_return(double("stat", :gid => 1001, :uid => 101)) - end - - context "#{service_name}" do - let(:new_resource) { Chef::Resource::Service.new(service_name) } - let!(:current_resource) { Chef::Resource::Service.new(service_name) } - - describe "#load_current_resource" do - - # CHEF-5223 "you can't glob for a file that hasn't been converged - # onto the node yet." - context "when the plist doesn't exist" do - - def run_resource_setup_for_action(action) - new_resource.action(action) - provider.action = action - provider.load_current_resource - provider.define_resource_requirements - provider.process_resource_requirements - end - - before do - allow(Dir).to receive(:glob).and_return([]) - allow(provider).to receive(:shell_out!). - with(/plutil -convert xml1 -o/). - and_raise(Mixlib::ShellOut::ShellCommandFailed) - end - - it "works for action :nothing" do - expect { run_resource_setup_for_action(:nothing) }.not_to raise_error - end - - it "works for action :start" do - expect { run_resource_setup_for_action(:start) }.not_to raise_error - end - - it "errors if action is :enable" do - expect { run_resource_setup_for_action(:enable) }.to raise_error(Chef::Exceptions::Service) - end - - it "errors if action is :disable" do - expect { run_resource_setup_for_action(:disable) }.to raise_error(Chef::Exceptions::Service) + ["Daemon", "Agent"].each do |service_type| + ["redis-server", "io.redis.redis-server"].each do |service_name| + ["10.9", "10.10", "10.11"].each do |platform_version| + let(:plist) {'/Library/LaunchDaemons/io.redis.redis-server.plist'} + let(:session) { StringIO.new } + if service_type == 'Agent' + let(:plist) {'/Library/LaunchAgents/io.redis.redis-server.plist'} + let(:session) {'-S Aqua '} + let(:su_cmd) {'su -l igor -c'} + if platform_version == "10.9" + let(:su_cmd) {'su igor -c'} end end - - context "when launchctl returns pid in service list" do - let(:launchctl_stdout) { StringIO.new <<-SVC_LIST } - 12761 - 0x100114220.old.machinit.thing - 7777 - io.redis.redis-server - - - com.lol.stopped-thing - SVC_LIST - - before do - provider.load_current_resource - end - - it "sets resource running state to true" do - expect(provider.current_resource.running).to be_truthy - end - - it "sets resouce enabled state to true" do - expect(provider.current_resource.enabled).to be_truthy - end + let(:service_label) {'io.redis.redis-server'} + before do + allow(Dir).to receive(:glob).and_return([plist], []) + allow(Etc).to receive(:getlogin).and_return('igor') + allow(node).to receive(:[]).with("platform_version").and_return(platform_version) + cmd = "launchctl list #{service_label}" + allow(provider).to receive(:shell_out_with_systems_locale). + with(/(#{su_cmd} '#{cmd}'|#{cmd})/). + and_return(double("Status", + :stdout => launchctl_stdout, :exitstatus => 0)) + allow(File).to receive(:exists?).and_return([true], []) + allow(provider).to receive(:shell_out_with_systems_locale!). + with(/plutil -convert xml1 -o/). + and_return(double("Status", :stdout => plutil_stdout)) end - describe "running unsupported actions" do - let(:launchctl_stdout) { StringIO.new <<-SVC_LIST } -12761 - 0x100114220.old.machinit.thing -7777 - io.redis.redis-server -- - com.lol.stopped-thing + context "#{service_name} that is a #{service_type} running Osx #{platform_version}" do + let(:new_resource) { Chef::Resource::MacosxService.new(service_name) } + let!(:current_resource) { Chef::Resource::MacosxService.new(service_name) } + + describe "#load_current_resource" do + + # CHEF-5223 "you can't glob for a file that hasn't been converged + # onto the node yet." + context "when the plist doesn't exist" do + + def run_resource_setup_for_action(action) + new_resource.action(action) + provider.action = action + provider.load_current_resource + provider.define_resource_requirements + provider.process_resource_requirements + end + + before do + allow(Dir).to receive(:glob).and_return([]) + allow(File).to receive(:exists?).and_return([true], []) + allow(provider).to receive(:shell_out!). + with(/plutil -convert xml1 -o/). + and_raise(Mixlib::ShellOut::ShellCommandFailed) + end + + it "works for action :nothing" do + expect { run_resource_setup_for_action(:nothing) }.not_to raise_error + end + + it "works for action :start" do + expect { run_resource_setup_for_action(:start) }.not_to raise_error + end + + it "errors if action is :enable" do + expect { run_resource_setup_for_action(:enable) }.to raise_error(Chef::Exceptions::Service) + end + + it "errors if action is :disable" do + expect { run_resource_setup_for_action(:disable) }.to raise_error(Chef::Exceptions::Service) + end + end + + context "when launchctl returns pid in service list" do + let(:launchctl_stdout) { StringIO.new <<-SVC_LIST } +{ + "LimitLoadToSessionType" = "System"; + "Label" = "io.redis.redis-server"; + "TimeOut" = 30; + "OnDemand" = false; + "LastExitStatus" = 0; + "PID" = 62803; + "Program" = "do_some.sh"; + "ProgramArguments" = ( + "path/to/do_something.sh"; + "-f"; + ); +}; SVC_LIST - before do - allow(Dir).to receive(:glob).and_return(["/Users/igor/Library/LaunchAgents/io.redis.redis-server.plist"], []) - end - it "should throw an exception when reload action is attempted" do - expect {provider.run_action(:reload)}.to raise_error(Chef::Exceptions::UnsupportedAction) - end - end - context "when launchctl returns empty service pid" do - let(:launchctl_stdout) { StringIO.new <<-SVC_LIST } - 12761 - 0x100114220.old.machinit.thing - - - io.redis.redis-server - - - com.lol.stopped-thing - SVC_LIST - - before do - provider.load_current_resource - end + before do + provider.load_current_resource + end - it "sets resource running state to false" do - expect(provider.current_resource.running).to be_falsey - end + it "sets resource running state to true" do + expect(provider.current_resource.running).to be_truthy + end - it "sets resouce enabled state to true" do - expect(provider.current_resource.enabled).to be_truthy - end - end + it "sets resouce enabled state to true" do + expect(provider.current_resource.enabled).to be_truthy + end + end - context "when launchctl doesn't return service entry at all" do - let(:launchctl_stdout) { StringIO.new <<-SVC_LIST } - 12761 - 0x100114220.old.machinit.thing - - - com.lol.stopped-thing - SVC_LIST + describe "running unsupported actions" do + before do + allow(Dir).to receive(:glob).and_return(["#{plist}"], []) + allow(File).to receive(:exists?).and_return([true], []) + end + it "should throw an exception when reload action is attempted" do + expect {provider.run_action(:reload)}.to raise_error(Chef::Exceptions::UnsupportedAction) + end + end + context "when launchctl returns empty service pid" do + let(:launchctl_stdout) { StringIO.new <<-SVC_LIST } +{ + "LimitLoadToSessionType" = "System"; + "Label" = "io.redis.redis-server"; + "TimeOut" = 30; + "OnDemand" = false; + "LastExitStatus" = 0; + "Program" = "do_some.sh"; + "ProgramArguments" = ( + "path/to/do_something.sh"; + "-f"; + ); +}; +SVC_LIST - it "sets service running state to false" do - provider.load_current_resource - expect(provider.current_resource.running).to be_falsey - end + before do + provider.load_current_resource + end - context "and plist for service is not available" do - before do - allow(Dir).to receive(:glob).and_return([]) - provider.load_current_resource - end + it "sets resource running state to false" do + expect(provider.current_resource.running).to be_falsey + end - it "sets resouce enabled state to false" do - expect(provider.current_resource.enabled).to be_falsey + it "sets resouce enabled state to true" do + expect(provider.current_resource.enabled).to be_truthy + end end - end - context "and plist for service is available" do - before do - allow(Dir).to receive(:glob).and_return(["/Users/igor/Library/LaunchAgents/io.redis.redis-server.plist"], []) - provider.load_current_resource - end + context "when launchctl doesn't return service entry at all" do + let(:launchctl_stdout) { StringIO.new <<-SVC_LIST } +Could not find service "io.redis.redis-server" in domain for system +SVC_LIST - it "sets resouce enabled state to true" do - expect(provider.current_resource.enabled).to be_truthy + it "sets service running state to false" do + provider.load_current_resource + expect(provider.current_resource.running).to be_falsey + end + + context "and plist for service is not available" do + before do + allow(Dir).to receive(:glob).and_return([]) + provider.load_current_resource + end + + it "sets resouce enabled state to false" do + expect(provider.current_resource.enabled).to be_falsey + end + end + + context "and plist for service is available" do + before do + allow(Dir).to receive(:glob).and_return(["#{plist}"], []) + provider.load_current_resource + end + + it "sets resouce enabled state to true" do + expect(provider.current_resource.enabled).to be_truthy + end + end + + describe "and several plists match service name" do + it "throws exception" do + allow(Dir).to receive(:glob).and_return(["#{plist}", + "/Users/wtf/something.plist"]) + provider.load_current_resource + provider.define_resource_requirements + expect { provider.process_resource_requirements }.to raise_error(Chef::Exceptions::Service) + end + end end end - - describe "and several plists match service name" do - it "throws exception" do - allow(Dir).to receive(:glob).and_return(["/Users/igor/Library/LaunchAgents/io.redis.redis-server.plist", - "/Users/wtf/something.plist"]) + describe "#start_service" do + before do + allow(Chef::Resource::MacosxService).to receive(:new).and_return(current_resource) provider.load_current_resource - provider.define_resource_requirements - expect { provider.process_resource_requirements }.to raise_error(Chef::Exceptions::Service) + allow(current_resource).to receive(:running).and_return(false) end - end - end - end - describe "#start_service" do - before do - allow(Chef::Resource::Service).to receive(:new).and_return(current_resource) - provider.load_current_resource - allow(current_resource).to receive(:running).and_return(false) - end - it "calls the start command if one is specified and service is not running" do - allow(new_resource).to receive(:start_command).and_return("cowsay dirty") + it "calls the start command if one is specified and service is not running" do + allow(new_resource).to receive(:start_command).and_return("cowsay dirty") - expect(provider).to receive(:shell_out_with_systems_locale!).with("cowsay dirty") - provider.start_service - end + expect(provider).to receive(:shell_out_with_systems_locale!).with("cowsay dirty") + provider.start_service + end - it "shows warning message if service is already running" do - allow(current_resource).to receive(:running).and_return(true) - expect(Chef::Log).to receive(:debug).with("service[#{service_name}] already running, not starting") + it "shows warning message if service is already running" do + allow(current_resource).to receive(:running).and_return(true) + expect(Chef::Log).to receive(:debug).with("macosx_service[#{service_name}] already running, not starting") - provider.start_service - end + provider.start_service + end - it "starts service via launchctl if service found" do - expect(provider).to receive(:shell_out_with_systems_locale!). - with("launchctl load -w '/Users/igor/Library/LaunchAgents/io.redis.redis-server.plist'", - :group => 1001, :user => 101). - and_return(0) + it "starts service via launchctl if service found" do + cmd = 'launchctl load -w ' + session + plist + expect(provider).to receive(:shell_out_with_systems_locale). + with(/(#{su_cmd} .#{cmd}.|#{cmd})/). + and_return(0) - provider.start_service - end - end + provider.start_service + end + end - describe "#stop_service" do - before do - allow(Chef::Resource::Service).to receive(:new).and_return(current_resource) + describe "#stop_service" do + before do + allow(Chef::Resource::MacosxService).to receive(:new).and_return(current_resource) - provider.load_current_resource - allow(current_resource).to receive(:running).and_return(true) - end + provider.load_current_resource + allow(current_resource).to receive(:running).and_return(true) + end - it "calls the stop command if one is specified and service is running" do - allow(new_resource).to receive(:stop_command).and_return("kill -9 123") + it "calls the stop command if one is specified and service is running" do + allow(new_resource).to receive(:stop_command).and_return("kill -9 123") - expect(provider).to receive(:shell_out_with_systems_locale!).with("kill -9 123") - provider.stop_service - end + expect(provider).to receive(:shell_out_with_systems_locale!).with("kill -9 123") + provider.stop_service + end - it "shows warning message if service is not running" do - allow(current_resource).to receive(:running).and_return(false) - expect(Chef::Log).to receive(:debug).with("service[#{service_name}] not running, not stopping") + it "shows warning message if service is not running" do + allow(current_resource).to receive(:running).and_return(false) + expect(Chef::Log).to receive(:debug).with("macosx_service[#{service_name}] not running, not stopping") - provider.stop_service - end + provider.stop_service + end - it "stops the service via launchctl if service found" do - expect(provider).to receive(:shell_out_with_systems_locale!). - with("launchctl unload '/Users/igor/Library/LaunchAgents/io.redis.redis-server.plist'", - :group => 1001, :user => 101). - and_return(0) + it "stops the service via launchctl if service found" do + cmd = 'launchctl unload -w '+ plist + expect(provider).to receive(:shell_out_with_systems_locale). + with(/(#{su_cmd} .#{cmd}.|#{cmd})/). + and_return(0) - provider.stop_service - end - end + provider.stop_service + end + end - describe "#restart_service" do - before do - allow(Chef::Resource::Service).to receive(:new).and_return(current_resource) + describe "#restart_service" do + before do + allow(Chef::Resource::Service).to receive(:new).and_return(current_resource) - provider.load_current_resource - allow(current_resource).to receive(:running).and_return(true) - allow(provider).to receive(:sleep) - end + provider.load_current_resource + allow(current_resource).to receive(:running).and_return(true) + allow(provider).to receive(:sleep) + end - it "issues a command if given" do - allow(new_resource).to receive(:restart_command).and_return("reload that thing") + it "issues a command if given" do + allow(new_resource).to receive(:restart_command).and_return("reload that thing") - expect(provider).to receive(:shell_out_with_systems_locale!).with("reload that thing") - provider.restart_service - end + expect(provider).to receive(:shell_out_with_systems_locale!).with("reload that thing") + provider.restart_service + end - it "stops and then starts service" do - expect(provider).to receive(:stop_service) - expect(provider).to receive(:start_service); + it "stops and then starts service" do + expect(provider).to receive(:unload_service) + expect(provider).to receive(:load_service); - provider.restart_service + provider.restart_service + end + end end end end diff --git a/spec/unit/provider/service/openbsd_service_spec.rb b/spec/unit/provider/service/openbsd_service_spec.rb index 1b5206470e..d3c150a14b 100644 --- a/spec/unit/provider/service/openbsd_service_spec.rb +++ b/spec/unit/provider/service/openbsd_service_spec.rb @@ -35,10 +35,12 @@ describe Chef::Provider::Service::Openbsd do node end + let(:supports) { {:status => false} } + let(:new_resource) do new_resource = Chef::Resource::Service.new("sndiod") new_resource.pattern("sndiod") - new_resource.supports({:status => false}) + new_resource.supports(supports) new_resource end @@ -106,9 +108,7 @@ describe Chef::Provider::Service::Openbsd do context "when the service supports status" do let(:status) { double(:stdout => "", :exitstatus => 0) } - before do - new_resource.supports({:status => true}) - end + let(:supports) { { :status => true } } it "should run '/etc/rc.d/service_name status'" do expect(provider).to receive(:shell_out).with("/etc/rc.d/#{new_resource.service_name} check").and_return(status) @@ -305,10 +305,12 @@ describe Chef::Provider::Service::Openbsd do end describe Chef::Provider::Service::Openbsd, "restart_service" do - it "should call 'restart' on the service_name if the resource supports it" do - new_resource.supports({:restart => true}) - expect(provider).to receive(:shell_out_with_systems_locale!).with("/etc/rc.d/#{new_resource.service_name} restart") - provider.restart_service() + context "when the new_resource supports restart" do + let(:supports) { { restart: true } } + it "should call 'restart' on the service_name if the resource supports it" do + expect(provider).to receive(:shell_out_with_systems_locale!).with("/etc/rc.d/#{new_resource.service_name} restart") + provider.restart_service() + end end it "should call the restart_command if one has been specified" do diff --git a/spec/unit/provider/service/redhat_spec.rb b/spec/unit/provider/service/redhat_spec.rb index 73cfec8a6f..5aaf54d9f5 100644 --- a/spec/unit/provider/service/redhat_spec.rb +++ b/spec/unit/provider/service/redhat_spec.rb @@ -64,24 +64,76 @@ describe "Chef::Provider::Service::Redhat" do end describe "load current resource" do - it "sets the current enabled status to true if the service is enabled for any run level" do + before do status = double("Status", :exitstatus => 0, :stdout => "" , :stderr => "") - expect(@provider).to receive(:shell_out).with("/sbin/service chef status").and_return(status) + allow(@provider).to receive(:shell_out).with("/sbin/service chef status").and_return(status) + end + + it "sets supports[:status] to true by default" do chkconfig = double("Chkconfig", :exitstatus => 0, :stdout => "chef 0:off 1:off 2:off 3:off 4:off 5:on 6:off", :stderr => "") expect(@provider).to receive(:shell_out!).with("/sbin/chkconfig --list chef", :returns => [0,1]).and_return(chkconfig) - expect(@provider.instance_variable_get("@service_missing")).to be_falsey + expect(@provider.service_missing).to be false @provider.load_current_resource - expect(@current_resource.enabled).to be_truthy + expect(@provider.supports[:status]).to be true + end + + it "lets the user override supports[:status] in the new_resource" do + @new_resource.supports( { status: false } ) + @new_resource.pattern "myservice" + chkconfig = double("Chkconfig", :exitstatus => 0, :stdout => "chef 0:off 1:off 2:off 3:off 4:off 5:on 6:off", :stderr => "") + expect(@provider).to receive(:shell_out!).with("/sbin/chkconfig --list chef", :returns => [0,1]).and_return(chkconfig) + foo_out = double("ps_command", :exitstatus => 0, :stdout => "a line that matches myservice", :stderr => "") + expect(@provider).to receive(:shell_out!).with("foo").and_return(foo_out) + expect(@provider.service_missing).to be false + expect(@provider).not_to receive(:shell_out).with("/sbin/service chef status") + @provider.load_current_resource + expect(@provider.supports[:status]).to be false + end + + it "sets the current enabled status to true if the service is enabled for any run level" do + chkconfig = double("Chkconfig", :exitstatus => 0, :stdout => "chef 0:off 1:off 2:off 3:off 4:off 5:on 6:off", :stderr => "") + expect(@provider).to receive(:shell_out!).with("/sbin/chkconfig --list chef", :returns => [0,1]).and_return(chkconfig) + expect(@provider.service_missing).to be false + @provider.load_current_resource + expect(@current_resource.enabled).to be true end it "sets the current enabled status to false if the regex does not match" do - status = double("Status", :exitstatus => 0, :stdout => "" , :stderr => "") - expect(@provider).to receive(:shell_out).with("/sbin/service chef status").and_return(status) chkconfig = double("Chkconfig", :exitstatus => 0, :stdout => "chef 0:off 1:off 2:off 3:off 4:off 5:off 6:off", :stderr => "") expect(@provider).to receive(:shell_out!).with("/sbin/chkconfig --list chef", :returns => [0,1]).and_return(chkconfig) - expect(@provider.instance_variable_get("@service_missing")).to be_falsey + expect(@provider.service_missing).to be false expect(@provider.load_current_resource).to eql(@current_resource) - expect(@current_resource.enabled).to be_falsey + expect(@current_resource.enabled).to be false + end + + it "sets the current enabled status to true if the service is enabled at specified run levels" do + @new_resource.run_levels([1, 2]) + chkconfig = double("Chkconfig", :exitstatus => 0, :stdout => "chef 0:off 1:on 2:on 3:off 4:off 5:off 6:off", :stderr => "") + expect(@provider).to receive(:shell_out!).with("/sbin/chkconfig --list chef", :returns => [0,1]).and_return(chkconfig) + expect(@provider.service_missing).to be false + @provider.load_current_resource + expect(@current_resource.enabled).to be true + expect(@provider.current_run_levels).to eql([1, 2]) + end + + it "sets the current enabled status to false if the service is enabled at a run level it should not" do + @new_resource.run_levels([1, 2]) + chkconfig = double("Chkconfig", :exitstatus => 0, :stdout => "chef 0:off 1:on 2:on 3:on 4:off 5:off 6:off", :stderr => "") + expect(@provider).to receive(:shell_out!).with("/sbin/chkconfig --list chef", :returns => [0,1]).and_return(chkconfig) + expect(@provider.service_missing).to be false + @provider.load_current_resource + expect(@current_resource.enabled).to be false + expect(@provider.current_run_levels).to eql([1, 2, 3]) + end + + it "sets the current enabled status to false if the service is not enabled at specified run levels" do + @new_resource.run_levels([ 2 ]) + chkconfig = double("Chkconfig", :exitstatus => 0, :stdout => "chef 0:off 1:on 2:off 3:off 4:off 5:off 6:off", :stderr => "") + expect(@provider).to receive(:shell_out!).with("/sbin/chkconfig --list chef", :returns => [0,1]).and_return(chkconfig) + expect(@provider.service_missing).to be false + @provider.load_current_resource + expect(@current_resource.enabled).to be false + expect(@provider.current_run_levels).to eql([1]) end end @@ -144,6 +196,28 @@ describe "Chef::Provider::Service::Redhat" do expect(@provider).to receive(:shell_out!).with("/sbin/chkconfig #{@new_resource.service_name} on") @provider.enable_service end + + it "should call chkconfig to add 'service_name' at specified run_levels" do + allow(@provider).to receive(:run_levels).and_return([1, 2]) + expect(@provider).to receive(:shell_out!).with("/sbin/chkconfig --level 12 #{@new_resource.service_name} on") + @provider.enable_service + end + + it "should call chkconfig to add 'service_name' at specified run_levels when run_levels do not match" do + allow(@provider).to receive(:run_levels).and_return([1, 2]) + allow(@provider).to receive(:current_run_levels).and_return([1, 3]) + expect(@provider).to receive(:shell_out!).with("/sbin/chkconfig --level 12 #{@new_resource.service_name} on") + expect(@provider).to receive(:shell_out!).with("/sbin/chkconfig --level 3 #{@new_resource.service_name} off") + @provider.enable_service + end + + it "should call chkconfig to add 'service_name' at specified run_levels if there is an extra run_level" do + allow(@provider).to receive(:run_levels).and_return([1, 2]) + allow(@provider).to receive(:current_run_levels).and_return([1, 2, 3]) + expect(@provider).to receive(:shell_out!).with("/sbin/chkconfig --level 12 #{@new_resource.service_name} on") + expect(@provider).to receive(:shell_out!).with("/sbin/chkconfig --level 3 #{@new_resource.service_name} off") + @provider.enable_service + end end describe "disable_service" do @@ -151,6 +225,12 @@ describe "Chef::Provider::Service::Redhat" do expect(@provider).to receive(:shell_out!).with("/sbin/chkconfig #{@new_resource.service_name} off") @provider.disable_service end + + it "should call chkconfig to del 'service_name' at specified run_levels" do + allow(@provider).to receive(:run_levels).and_return([1, 2]) + expect(@provider).to receive(:shell_out!).with("/sbin/chkconfig --level 12 #{@new_resource.service_name} off") + @provider.disable_service + end end end diff --git a/spec/unit/provider/service/upstart_service_spec.rb b/spec/unit/provider/service/upstart_service_spec.rb index ca7ce8f930..1c8e304cb7 100644 --- a/spec/unit/provider/service/upstart_service_spec.rb +++ b/spec/unit/provider/service/upstart_service_spec.rb @@ -19,6 +19,10 @@ require 'spec_helper' describe Chef::Provider::Service::Upstart do + let(:shell_out_success) do + double('shell_out_with_systems_locale', :exitstatus => 0, :error? => false) + end + before(:each) do @node =Chef::Node.new @node.name('upstarter') @@ -173,7 +177,7 @@ describe Chef::Provider::Service::Upstart do end it "should run the services status command if one has been specified" do - allow(@provider).to receive(:shell_out!).with("/bin/chefhasmonkeypants status").and_return(0) + allow(@provider).to receive(:shell_out!).with("/bin/chefhasmonkeypants status").and_return(shell_out_success) expect(@current_resource).to receive(:running).with(true) @provider.load_current_resource end @@ -246,7 +250,7 @@ describe Chef::Provider::Service::Upstart do end it "should call '/sbin/start service_name' if no start command is specified" do - expect(@provider).to receive(:shell_out_with_systems_locale!).with("/sbin/start #{@new_resource.service_name}").and_return(0) + expect(@provider).to receive(:shell_out_with_systems_locale!).with("/sbin/start #{@new_resource.service_name}").and_return(shell_out_success) @provider.start_service() end @@ -261,7 +265,7 @@ describe Chef::Provider::Service::Upstart do @new_resource.parameters({ "OSD_ID" => "2" }) @provider = Chef::Provider::Service::Upstart.new(@new_resource, @run_context) @provider.current_resource = @current_resource - expect(@provider).to receive(:shell_out_with_systems_locale!).with("/sbin/start rsyslog OSD_ID=2").and_return(0) + expect(@provider).to receive(:shell_out_with_systems_locale!).with("/sbin/start rsyslog OSD_ID=2").and_return(shell_out_success) @provider.start_service() end @@ -274,13 +278,13 @@ describe Chef::Provider::Service::Upstart do it "should call '/sbin/restart service_name' if no restart command is specified" do allow(@current_resource).to receive(:running).and_return(true) - expect(@provider).to receive(:shell_out_with_systems_locale!).with("/sbin/restart #{@new_resource.service_name}").and_return(0) + expect(@provider).to receive(:shell_out_with_systems_locale!).with("/sbin/restart #{@new_resource.service_name}").and_return(shell_out_success) @provider.restart_service() end it "should call '/sbin/start service_name' if restart_service is called for a stopped service" do allow(@current_resource).to receive(:running).and_return(false) - expect(@provider).to receive(:shell_out_with_systems_locale!).with("/sbin/start #{@new_resource.service_name}").and_return(0) + expect(@provider).to receive(:shell_out_with_systems_locale!).with("/sbin/start #{@new_resource.service_name}").and_return(shell_out_success) @provider.restart_service() end @@ -293,7 +297,7 @@ describe Chef::Provider::Service::Upstart do it "should call '/sbin/reload service_name' if no reload command is specified" do allow(@current_resource).to receive(:running).and_return(true) - expect(@provider).to receive(:shell_out_with_systems_locale!).with("/sbin/reload #{@new_resource.service_name}").and_return(0) + expect(@provider).to receive(:shell_out_with_systems_locale!).with("/sbin/reload #{@new_resource.service_name}").and_return(shell_out_success) @provider.reload_service() end @@ -306,7 +310,7 @@ describe Chef::Provider::Service::Upstart do it "should call '/sbin/stop service_name' if no stop command is specified" do allow(@current_resource).to receive(:running).and_return(true) - expect(@provider).to receive(:shell_out_with_systems_locale!).with("/sbin/stop #{@new_resource.service_name}").and_return(0) + expect(@provider).to receive(:shell_out_with_systems_locale!).with("/sbin/stop #{@new_resource.service_name}").and_return(shell_out_success) @provider.stop_service() end diff --git a/spec/unit/provider/template/content_spec.rb b/spec/unit/provider/template/content_spec.rb index 4b88a3aea5..3d6e822c00 100644 --- a/spec/unit/provider/template/content_spec.rb +++ b/spec/unit/provider/template/content_spec.rb @@ -23,6 +23,10 @@ describe Chef::Provider::Template::Content do let(:new_resource) do double("Chef::Resource::Template (new)", :cookbook_name => 'openldap', + :recipe_name => 'default', + :source_line => "/Users/lamont/solo/cookbooks/openldap/recipes/default.rb:2:in `from_file'", + :source_line_file => "/Users/lamont/solo/cookbooks/openldap/recipes/default.rb", + :source_line_number => "2", :source => 'openldap_stuff.conf.erb', :local => false, :cookbook => nil, @@ -75,4 +79,41 @@ describe Chef::Provider::Template::Content do expect(IO.read(content.tempfile.path)).to eq("slappiness is a warm gun") end + describe "when using location helpers" do + let(:new_resource) do + double("Chef::Resource::Template (new)", + :cookbook_name => 'openldap', + :recipe_name => 'default', + :source_line => CHEF_SPEC_DATA + "/cookbooks/openldap/recipes/default.rb:2:in `from_file'", + :source_line_file => CHEF_SPEC_DATA + "/cookbooks/openldap/recipes/default.rb", + :source_line_number => "2", + :source => 'helpers.erb', + :local => false, + :cookbook => nil, + :variables => {}, + :inline_helper_blocks => {}, + :inline_helper_modules => [], + :helper_modules => []) + end + + it "creates the template with the rendered content" do + expect(IO.read(content.tempfile.path)).to eql <<EOF +openldap +default +#{CHEF_SPEC_DATA}/cookbooks/openldap/recipes/default.rb:2:in `from_file' +#{CHEF_SPEC_DATA}/cookbooks/openldap/recipes/default.rb +2 +helpers.erb +#{CHEF_SPEC_DATA}/cookbooks/openldap/templates/default/helpers.erb +openldap +default +#{CHEF_SPEC_DATA}/cookbooks/openldap/recipes/default.rb:2:in `from_file' +#{CHEF_SPEC_DATA}/cookbooks/openldap/recipes/default.rb +2 +helpers.erb +#{CHEF_SPEC_DATA}/cookbooks/openldap/templates/default/helpers.erb +EOF + end + + end end diff --git a/spec/unit/provider/user/dscl_spec.rb b/spec/unit/provider/user/dscl_spec.rb index 5ea037d944..32d0812d8c 100644 --- a/spec/unit/provider/user/dscl_spec.rb +++ b/spec/unit/provider/user/dscl_spec.rb @@ -24,7 +24,7 @@ require 'mixlib/shellout' describe Chef::Provider::User::Dscl do before do - allow(Chef::Platform).to receive(:windows?) { false } + allow(ChefConfig).to receive(:windows?) { false } end let(:node) { node = Chef::Node.new diff --git a/spec/unit/provider/user_spec.rb b/spec/unit/provider/user_spec.rb index 381168647b..2345ce18fb 100644 --- a/spec/unit/provider/user_spec.rb +++ b/spec/unit/provider/user_spec.rb @@ -143,8 +143,8 @@ describe Chef::Provider::User do begin require 'rubygems' require 'shadow' - rescue LoadError => e - pending "ruby-shadow gem not installed for dynamic load test" + rescue LoadError + skip "ruby-shadow gem not installed for dynamic load test" true else false @@ -161,7 +161,7 @@ describe Chef::Provider::User do unless shadow_lib_unavail? context "and we have the ruby-shadow gem" do - pending "and we are not root (rerun this again as root)", :requires_unprivileged_user => true + skip "and we are not root (rerun this again as root)", :requires_unprivileged_user => true context "and we are root", :requires_root => true do it "should pass assertions when ruby-shadow can be loaded" do diff --git a/spec/unit/provider_resolver_spec.rb b/spec/unit/provider_resolver_spec.rb index a9fa08ebfd..88df4a20cc 100644 --- a/spec/unit/provider_resolver_spec.rb +++ b/spec/unit/provider_resolver_spec.rb @@ -18,32 +18,118 @@ require 'spec_helper' require 'chef/mixin/convert_to_class_name' +require 'chef/provider_resolver' +require 'chef/platform/service_helpers' include Chef::Mixin::ConvertToClassName +# Open up Provider so we can write things down easier in here +#module Chef::Provider + describe Chef::ProviderResolver do + let(:resource_name) { :service } + let(:provider) { nil } + let(:action) { :start } + let(:node) do node = Chef::Node.new - allow(node).to receive(:[]).with(:os).and_return(os) - allow(node).to receive(:[]).with(:platform_family).and_return(platform_family) - allow(node).to receive(:[]).with(:platform).and_return(platform) - allow(node).to receive(:[]).with(:platform_version).and_return(platform_version) - allow(node).to receive(:is_a?).and_return(Chef::Node) + node.automatic[:os] = os + node.automatic[:platform_family] = platform_family + node.automatic[:platform] = platform + node.automatic[:platform_version] = platform_version + node.automatic[:kernel] = { machine: 'i386' } node end + let(:run_context) { Chef::RunContext.new(node, nil, nil) } let(:provider_resolver) { Chef::ProviderResolver.new(node, resource, action) } + let(:resolved_provider) do + begin + resource ? resource.provider_for_action(action).class : nil + rescue Chef::Exceptions::ProviderNotFound + nil + end + end - let(:action) { :start } + let(:resource) do + resource_class = Chef::ResourceResolver.resolve(resource_name, node: node) + if resource_class + resource = resource_class.new('test', run_context) + resource.provider = provider if provider + end + resource + end - let(:resolved_provider) { provider_resolver.resolve } + def self.on_platform(platform, *tags, + platform_version: '11.0.1', + platform_family: nil, + os: nil, + &block) + Array(platform).each do |platform| + Array(platform_version).each do |platform_version| + on_one_platform(platform, platform_version, platform_family || platform, os || platform_family || platform, *tags, &block) + end + end + end - let(:provider) { nil } + def self.on_one_platform(platform, platform_version, platform_family, os, *tags, &block) + describe "on #{platform} #{platform_version}, platform_family: #{platform_family}, os: #{os}", *tags do + let(:os) { os } + let(:platform) { platform } + let(:platform_family) { platform_family } + let(:platform_version) { platform_version } - let(:resource_name) { :service } + define_singleton_method(:os) { os } + define_singleton_method(:platform) { platform } + define_singleton_method(:platform_family) { platform_family } + define_singleton_method(:platform_version) { platform_version } - let(:resource) { double(Chef::Resource, provider: provider, resource_name: resource_name) } + instance_eval(&block) + end + end + + def self.expect_providers(**providers) + providers.each do |name, expected| + describe name.to_s do + let(:resource_name) { name } + + tags = [] + expected_provider = nil + expected_resource = nil + Array(expected).each do |p| + if p.is_a?(Class) && p <= Chef::Provider + expected_provider = p + elsif p.is_a?(Class) && p <= Chef::Resource + expected_resource = p + else + tags << p + end + end + + if expected_resource && expected_provider + it "'#{name}' resolves to resource #{expected_resource} and provider #{expected_provider}", *tags do + expect(resource.class).to eql(expected_resource) + provider = double(expected_provider, class: expected_provider) + expect(provider).to receive(:action=).with(action) + expect(expected_provider).to receive(:new).with(resource, run_context).and_return(provider) + expect(resolved_provider).to eql(expected_provider) + end + elsif expected_provider + it "'#{name}' resolves to provider #{expected_provider}", *tags do + provider = double(expected_provider) + expect(provider).to receive(:action=).with(action) + expect(expected_provider).to receive(:new).with(resource, run_context).and_return(provider) + expect(resolved_provider).to eql(expected_provider) + end + else + it "'#{name}' fails to resolve (since #{name.inspect} is unsupported on #{platform} #{platform_version})", *tags do + expect(resolved_provider).to be_nil + end + end + end + end + end describe "resolving service resource" do def stub_service_providers(*services) @@ -59,7 +145,6 @@ describe Chef::ProviderResolver do end before do - expect(provider_resolver).not_to receive(:maybe_chef_platform_lookup) allow(resource).to receive(:service_name).and_return("ntp") end @@ -296,257 +381,479 @@ describe Chef::ProviderResolver do end end - describe "on Ubuntu 14.10" do - let(:os) { "linux" } - let(:platform) { "ubuntu" } - let(:platform_family) { "debian" } - let(:platform_version) { "14.04" } - + on_platform "ubuntu", platform_version: "14.10", platform_family: "debian", os: "linux" do it_behaves_like "an ubuntu platform with upstart, update-rc.d and systemd" end - describe "on Ubuntu 14.04" do - let(:os) { "linux" } - let(:platform) { "ubuntu" } - let(:platform_family) { "debian" } - let(:platform_version) { "14.04" } - + on_platform "ubuntu", platform_version: "14.04", platform_family: "debian", os: "linux" do it_behaves_like "an ubuntu platform with upstart and update-rc.d" end - describe "on Ubuntu 10.04" do - let(:os) { "linux" } - let(:platform) { "ubuntu" } - let(:platform_family) { "debian" } - let(:platform_version) { "10.04" } - + on_platform "ubuntu", platform_version: "10.04", platform_family: "debian", os: "linux" do it_behaves_like "an ubuntu platform with upstart and update-rc.d" end # old debian uses the Debian provider (does not have insserv or upstart, or update-rc.d???) - describe "on Debian 4.0" do - let(:os) { "linux" } - let(:platform) { "debian" } - let(:platform_family) { "debian" } - let(:platform_version) { "4.0" } - + on_platform "debian", platform_version: "4.0", os: "linux" do #it_behaves_like "a debian platform using the debian provider" end # Debian replaced the debian provider with insserv in the FIXME:VERSION distro - describe "on Debian 7.0" do - let(:os) { "linux" } - let(:platform) { "debian" } - let(:platform_family) { "debian" } - let(:platform_version) { "7.0" } - + on_platform "debian", platform_version: "7.0", os: "linux" do it_behaves_like "a debian platform using the insserv provider" end - %w{solaris2 openindiana opensolaris nexentacore omnios smartos}.each do |platform| - describe "on #{platform}" do - let(:os) { "solaris2" } - let(:platform) { platform } - let(:platform_family) { platform } - let(:platform_version) { "5.11" } - - it "returns a Solaris provider" do - stub_service_providers - stub_service_configs - expect(resolved_provider).to eql(Chef::Provider::Service::Solaris) - end + on_platform %w{solaris2 openindiana opensolaris nexentacore omnios smartos}, os: "solaris2", platform_version: "5.11" do + it "returns a Solaris provider" do + stub_service_providers + stub_service_configs + expect(resolved_provider).to eql(Chef::Provider::Service::Solaris) + end - it "always returns a Solaris provider" do - # no matter what we stub on the next two lines we should get a Solaris provider - stub_service_providers(:debian, :invokercd, :insserv, :upstart, :redhat, :systemd) - stub_service_configs(:initd, :upstart, :xinetd, :user_local_etc_rcd, :systemd) - expect(resolved_provider).to eql(Chef::Provider::Service::Solaris) - end + it "always returns a Solaris provider" do + # no matter what we stub on the next two lines we should get a Solaris provider + stub_service_providers(:debian, :invokercd, :insserv, :upstart, :redhat, :systemd) + stub_service_configs(:initd, :upstart, :xinetd, :user_local_etc_rcd, :systemd) + expect(resolved_provider).to eql(Chef::Provider::Service::Solaris) end end - %w{mswin mingw32 windows}.each do |platform| - describe "on #{platform}" do - let(:os) { "windows" } - let(:platform) { platform } - let(:platform_family) { "windows" } - let(:platform_version) { "5.11" } - - it "returns a Windows provider" do - stub_service_providers - stub_service_configs - expect(resolved_provider).to eql(Chef::Provider::Service::Windows) - end + on_platform %w{mswin mingw32 windows}, platform_family: "windows", platform_version: "5.11" do + it "returns a Windows provider" do + stub_service_providers + stub_service_configs + expect(resolved_provider).to eql(Chef::Provider::Service::Windows) + end - it "always returns a Windows provider" do - # no matter what we stub on the next two lines we should get a Windows provider - stub_service_providers(:debian, :invokercd, :insserv, :upstart, :redhat, :systemd) - stub_service_configs(:initd, :upstart, :xinetd, :user_local_etc_rcd, :systemd) - expect(resolved_provider).to eql(Chef::Provider::Service::Windows) - end + it "always returns a Windows provider" do + # no matter what we stub on the next two lines we should get a Windows provider + stub_service_providers(:debian, :invokercd, :insserv, :upstart, :redhat, :systemd) + stub_service_configs(:initd, :upstart, :xinetd, :user_local_etc_rcd, :systemd) + expect(resolved_provider).to eql(Chef::Provider::Service::Windows) end end - %w{mac_os_x mac_os_x_server}.each do |platform| - describe "on #{platform}" do - let(:os) { "darwin" } - let(:platform) { platform } - let(:platform_family) { "mac_os_x" } - let(:platform_version) { "10.9.2" } - - it "returns a Macosx provider" do - stub_service_providers - stub_service_configs - expect(resolved_provider).to eql(Chef::Provider::Service::Macosx) - end + on_platform %w{mac_os_x mac_os_x_server}, os: "darwin", platform_family: "mac_os_x", platform_version: "10.9.2" do + it "returns a Macosx provider" do + stub_service_providers + stub_service_configs + expect(resolved_provider).to eql(Chef::Provider::Service::Macosx) + end - it "always returns a Macosx provider" do - # no matter what we stub on the next two lines we should get a Macosx provider - stub_service_providers(:debian, :invokercd, :insserv, :upstart, :redhat, :systemd) - stub_service_configs(:initd, :upstart, :xinetd, :user_local_etc_rcd, :systemd) - expect(resolved_provider).to eql(Chef::Provider::Service::Macosx) - end + it "always returns a Macosx provider" do + # no matter what we stub on the next two lines we should get a Macosx provider + stub_service_providers(:debian, :invokercd, :insserv, :upstart, :redhat, :systemd) + stub_service_configs(:initd, :upstart, :xinetd, :user_local_etc_rcd, :systemd) + expect(resolved_provider).to eql(Chef::Provider::Service::Macosx) end end - %w{freebsd netbsd}.each do |platform| - describe "on #{platform}" do - let(:os) { platform } - let(:platform) { platform } - let(:platform_family) { platform } - let(:platform_version) { "10.0-RELEASE" } - - it "returns a Freebsd provider if it finds the /usr/local/etc/rc.d initscript" do - stub_service_providers - stub_service_configs(:usr_local_etc_rcd) - expect(resolved_provider).to eql(Chef::Provider::Service::Freebsd) - end + on_platform %w(freebsd netbsd), platform_version: '3.1.4' do + it "returns a Freebsd provider if it finds the /usr/local/etc/rc.d initscript" do + stub_service_providers + stub_service_configs(:usr_local_etc_rcd) + expect(resolved_provider).to eql(Chef::Provider::Service::Freebsd) + end - it "returns a Freebsd provider if it finds the /etc/rc.d initscript" do - stub_service_providers - stub_service_configs(:etc_rcd) - expect(resolved_provider).to eql(Chef::Provider::Service::Freebsd) - end + it "returns a Freebsd provider if it finds the /etc/rc.d initscript" do + stub_service_providers + stub_service_configs(:etc_rcd) + expect(resolved_provider).to eql(Chef::Provider::Service::Freebsd) + end - it "always returns a Freebsd provider if it finds the /usr/local/etc/rc.d initscript" do - # should only care about :usr_local_etc_rcd stub in the service configs - stub_service_providers(:debian, :invokercd, :insserv, :upstart, :redhat, :systemd) - stub_service_configs(:usr_local_etc_rcd, :initd, :upstart, :xinetd, :systemd) - expect(resolved_provider).to eql(Chef::Provider::Service::Freebsd) - end + it "always returns a Freebsd provider if it finds the /usr/local/etc/rc.d initscript" do + # should only care about :usr_local_etc_rcd stub in the service configs + stub_service_providers(:debian, :invokercd, :insserv, :upstart, :redhat, :systemd) + stub_service_configs(:usr_local_etc_rcd, :initd, :upstart, :xinetd, :systemd) + expect(resolved_provider).to eql(Chef::Provider::Service::Freebsd) + end - it "always returns a Freebsd provider if it finds the /usr/local/etc/rc.d initscript" do - # should only care about :etc_rcd stub in the service configs - stub_service_providers(:debian, :invokercd, :insserv, :upstart, :redhat, :systemd) - stub_service_configs(:etc_rcd, :initd, :upstart, :xinetd, :systemd) - expect(resolved_provider).to eql(Chef::Provider::Service::Freebsd) - end + it "always returns a Freebsd provider if it finds the /usr/local/etc/rc.d initscript" do + # should only care about :etc_rcd stub in the service configs + stub_service_providers(:debian, :invokercd, :insserv, :upstart, :redhat, :systemd) + stub_service_configs(:etc_rcd, :initd, :upstart, :xinetd, :systemd) + expect(resolved_provider).to eql(Chef::Provider::Service::Freebsd) + end - it "foo" do - stub_service_providers - stub_service_configs - expect(resolved_provider).to eql(Chef::Provider::Service::Freebsd) - end + it "foo" do + stub_service_providers + stub_service_configs + expect(resolved_provider).to eql(Chef::Provider::Service::Freebsd) end end end - describe "resolving static providers" do - def resource_class(resource) - Chef::Resource.const_get(convert_to_class_name(resource.to_s)) - end - static_mapping = { - apt_package: Chef::Provider::Package::Apt, - bash: Chef::Provider::Script, - bff_package: Chef::Provider::Package::Aix, - breakpoint: Chef::Provider::Breakpoint, - chef_gem: Chef::Provider::Package::Rubygems, - cookbook_file: Chef::Provider::CookbookFile, - csh: Chef::Provider::Script, - deploy: Chef::Provider::Deploy::Timestamped, - deploy_revision: Chef::Provider::Deploy::Revision, - directory: Chef::Provider::Directory, - dpkg_package: Chef::Provider::Package::Dpkg, - dsc_script: Chef::Provider::DscScript, - easy_install_package: Chef::Provider::Package::EasyInstall, - erl_call: Chef::Provider::ErlCall, - execute: Chef::Provider::Execute, - file: Chef::Provider::File, - gem_package: Chef::Provider::Package::Rubygems, - git: Chef::Provider::Git, - homebrew_package: Chef::Provider::Package::Homebrew, - http_request: Chef::Provider::HttpRequest, - ips_package: Chef::Provider::Package::Ips, - link: Chef::Provider::Link, - log: Chef::Provider::Log::ChefLog, - macports_package: Chef::Provider::Package::Macports, - pacman_package: Chef::Provider::Package::Pacman, - paludis_package: Chef::Provider::Package::Paludis, - perl: Chef::Provider::Script, - portage_package: Chef::Provider::Package::Portage, - python: Chef::Provider::Script, - remote_directory: Chef::Provider::RemoteDirectory, - route: Chef::Provider::Route, - rpm_package: Chef::Provider::Package::Rpm, - ruby: Chef::Provider::Script, - ruby_block: Chef::Provider::RubyBlock, - script: Chef::Provider::Script, - smartos_package: Chef::Provider::Package::SmartOS, - solaris_package: Chef::Provider::Package::Solaris, - subversion: Chef::Provider::Subversion, - template: Chef::Provider::Template, - timestamped_deploy: Chef::Provider::Deploy::Timestamped, - whyrun_safe_ruby_block: Chef::Provider::WhyrunSafeRubyBlock, - windows_package: Chef::Provider::Package::Windows, - windows_service: Chef::Provider::Service::Windows, - yum_package: Chef::Provider::Package::Yum, + PROVIDERS = + { + bash: [ Chef::Resource::Bash, Chef::Provider::Script ], + breakpoint: [ Chef::Resource::Breakpoint, Chef::Provider::Breakpoint ], + chef_gem: [ Chef::Resource::ChefGem, Chef::Provider::Package::Rubygems ], + cookbook_file: [ Chef::Resource::CookbookFile, Chef::Provider::CookbookFile ], + csh: [ Chef::Resource::Csh, Chef::Provider::Script ], + deploy: [ Chef::Resource::Deploy, Chef::Provider::Deploy::Timestamped ], + deploy_revision: [ Chef::Resource::DeployRevision, Chef::Provider::Deploy::Revision ], + directory: [ Chef::Resource::Directory, Chef::Provider::Directory ], + easy_install_package: [ Chef::Resource::EasyInstallPackage, Chef::Provider::Package::EasyInstall ], + erl_call: [ Chef::Resource::ErlCall, Chef::Provider::ErlCall ], + execute: [ Chef::Resource::Execute, Chef::Provider::Execute ], + file: [ Chef::Resource::File, Chef::Provider::File ], + gem_package: [ Chef::Resource::GemPackage, Chef::Provider::Package::Rubygems ], + git: [ Chef::Resource::Git, Chef::Provider::Git ], + group: [ Chef::Resource::Group, Chef::Provider::Group::Gpasswd ], + homebrew_package: [ Chef::Resource::HomebrewPackage, Chef::Provider::Package::Homebrew ], + http_request: [ Chef::Resource::HttpRequest, Chef::Provider::HttpRequest ], + ifconfig: [ Chef::Resource::Ifconfig, Chef::Provider::Ifconfig ], + link: [ Chef::Resource::Link, Chef::Provider::Link ], + log: [ Chef::Resource::Log, Chef::Provider::Log::ChefLog ], + macports_package: [ Chef::Resource::MacportsPackage, Chef::Provider::Package::Macports ], + mdadm: [ Chef::Resource::Mdadm, Chef::Provider::Mdadm ], + mount: [ Chef::Resource::Mount, Chef::Provider::Mount::Mount ], + perl: [ Chef::Resource::Perl, Chef::Provider::Script ], + portage_package: [ Chef::Resource::PortagePackage, Chef::Provider::Package::Portage ], + python: [ Chef::Resource::Python, Chef::Provider::Script ], + remote_directory: [ Chef::Resource::RemoteDirectory, Chef::Provider::RemoteDirectory ], + route: [ Chef::Resource::Route, Chef::Provider::Route ], + ruby: [ Chef::Resource::Ruby, Chef::Provider::Script ], + ruby_block: [ Chef::Resource::RubyBlock, Chef::Provider::RubyBlock ], + script: [ Chef::Resource::Script, Chef::Provider::Script ], + subversion: [ Chef::Resource::Subversion, Chef::Provider::Subversion ], + template: [ Chef::Resource::Template, Chef::Provider::Template ], + timestamped_deploy: [ Chef::Resource::TimestampedDeploy, Chef::Provider::Deploy::Timestamped ], + user: [ Chef::Resource::User, Chef::Provider::User::Useradd ], + whyrun_safe_ruby_block: [ Chef::Resource::WhyrunSafeRubyBlock, Chef::Provider::WhyrunSafeRubyBlock ], + + # We want to check that these are unsupported: + apt_package: nil, + bff_package: nil, + dpkg_package: nil, + dsc_script: nil, + ips_package: nil, + pacman_package: nil, + paludis_package: nil, + rpm_package: nil, + smartos_package: nil, + solaris_package: nil, + yum_package: nil, + windows_package: nil, + windows_service: nil, + + "linux" => { + apt_package: [ Chef::Resource::AptPackage, Chef::Provider::Package::Apt ], + dpkg_package: [ Chef::Resource::DpkgPackage, Chef::Provider::Package::Dpkg ], + pacman_package: [ Chef::Resource::PacmanPackage, Chef::Provider::Package::Pacman ], + paludis_package: [ Chef::Resource::PaludisPackage, Chef::Provider::Package::Paludis ], + rpm_package: [ Chef::Resource::RpmPackage, Chef::Provider::Package::Rpm ], + yum_package: [ Chef::Resource::YumPackage, Chef::Provider::Package::Yum ], + + "debian" => { + ifconfig: [ Chef::Resource::Ifconfig, Chef::Provider::Ifconfig::Debian ], + package: [ Chef::Resource::AptPackage, Chef::Provider::Package::Apt ], +# service: [ Chef::Resource::DebianService, Chef::Provider::Service::Debian ], + + "debian" => { + "7.0" => { + }, + "6.0" => { + ifconfig: [ Chef::Resource::Ifconfig, Chef::Provider::Ifconfig ], +# service: [ Chef::Resource::InsservService, Chef::Provider::Service::Insserv ], + }, + "5.0" => { + ifconfig: [ Chef::Resource::Ifconfig, Chef::Provider::Ifconfig ], + }, + }, + "gcel" => { + "3.1.4" => { + ifconfig: [ Chef::Resource::Ifconfig, Chef::Provider::Ifconfig ], + }, + }, + "linaro" => { + "3.1.4" => { + ifconfig: [ Chef::Resource::Ifconfig, Chef::Provider::Ifconfig ], + }, + }, + "linuxmint" => { + "3.1.4" => { + ifconfig: [ Chef::Resource::Ifconfig, Chef::Provider::Ifconfig ], +# service: [ Chef::Resource::UpstartService, Chef::Provider::Service::Upstart ], + }, + }, + "raspbian" => { + "3.1.4" => { + ifconfig: [ Chef::Resource::Ifconfig, Chef::Provider::Ifconfig ], + }, + }, + "ubuntu" => { + "11.10" => { + }, + "10.04" => { + ifconfig: [ Chef::Resource::Ifconfig, Chef::Provider::Ifconfig ], + }, + }, + }, + + "arch" => { + # TODO should be Chef::Resource::PacmanPackage + package: [ Chef::Resource::Package, Chef::Provider::Package::Pacman ], + + "arch" => { + "3.1.4" => { + } + }, + }, + + "freebsd" => { + group: [ Chef::Resource::Group, Chef::Provider::Group::Pw ], + user: [ Chef::Resource::User, Chef::Provider::User::Pw ], + + "freebsd" => { + "3.1.4" => { + }, + }, + }, + "suse" => { + group: [ Chef::Resource::Group, Chef::Provider::Group::Gpasswd ], + "suse" => { + "12.0" => { + }, + %w(11.1 11.2 11.3) => { + group: [ Chef::Resource::Group, Chef::Provider::Group::Suse ], + }, + }, + "opensuse" => { +# service: [ Chef::Resource::RedhatService, Chef::Provider::Service::Redhat ], + package: [ Chef::Resource::ZypperPackage, Chef::Provider::Package::Zypper ], + group: [ Chef::Resource::Group, Chef::Provider::Group::Usermod ], + "12.3" => { + }, + "12.2" => { + group: [ Chef::Resource::Group, Chef::Provider::Group::Suse ], + }, + }, + }, + + "gentoo" => { + # TODO should be Chef::Resource::PortagePackage + package: [ Chef::Resource::Package, Chef::Provider::Package::Portage ], + portage_package: [ Chef::Resource::PortagePackage, Chef::Provider::Package::Portage ], +# service: [ Chef::Resource::GentooService, Chef::Provider::Service::Gentoo ], + + "gentoo" => { + "3.1.4" => { + }, + }, + }, + + "rhel" => { +# service: [ Chef::Resource::SystemdService, Chef::Provider::Service::Systemd ], + package: [ Chef::Resource::YumPackage, Chef::Provider::Package::Yum ], + ifconfig: [ Chef::Resource::Ifconfig, Chef::Provider::Ifconfig::Redhat ], + + %w(amazon xcp xenserver ibm_powerkvm cloudlinux parallels) => { + "3.1.4" => { +# service: [ Chef::Resource::RedhatService, Chef::Provider::Service::Redhat ], + }, + }, + %w(redhat centos scientific oracle) => { + "7.0" => { + }, + "6.0" => { +# service: [ Chef::Resource::RedhatService, Chef::Provider::Service::Redhat ], + }, + }, + "fedora" => { + "15.0" => { + }, + "14.0" => { +# service: [ Chef::Resource::RedhatService, Chef::Provider::Service::Redhat ], + }, + }, + }, + + }, + + "darwin" => { + %w(mac_os_x mac_os_x_server) => { + group: [ Chef::Resource::Group, Chef::Provider::Group::Dscl ], + package: [ Chef::Resource::HomebrewPackage, Chef::Provider::Package::Homebrew ], + user: [ Chef::Resource::User, Chef::Provider::User::Dscl ], + + "mac_os_x" => { + "10.9.2" => { + }, + }, + }, + }, + + "windows" => { + batch: [ Chef::Resource::Batch, Chef::Provider::Batch ], + dsc_script: [ Chef::Resource::DscScript, Chef::Provider::DscScript ], + env: [ Chef::Resource::Env, Chef::Provider::Env::Windows ], + group: [ Chef::Resource::Group, Chef::Provider::Group::Windows ], + mount: [ Chef::Resource::Mount, Chef::Provider::Mount::Windows ], + package: [ Chef::Resource::WindowsPackage, Chef::Provider::Package::Windows ], + powershell_script: [ Chef::Resource::PowershellScript, Chef::Provider::PowershellScript ], + service: [ Chef::Resource::WindowsService, Chef::Provider::Service::Windows ], + user: [ Chef::Resource::User, Chef::Provider::User::Windows ], + windows_package: [ Chef::Resource::WindowsPackage, Chef::Provider::Package::Windows ], + windows_service: [ Chef::Resource::WindowsService, Chef::Provider::Service::Windows ], + + "windows" => { + %w(mswin mingw32 windows) => { + "10.9.2" => { + }, + }, + }, + }, + + "aix" => { + bff_package: [ Chef::Resource::BffPackage, Chef::Provider::Package::Aix ], + cron: [ Chef::Resource::Cron, Chef::Provider::Cron::Aix ], + group: [ Chef::Resource::Group, Chef::Provider::Group::Aix ], + ifconfig: [ Chef::Resource::Ifconfig, Chef::Provider::Ifconfig::Aix ], + mount: [ Chef::Resource::Mount, Chef::Provider::Mount::Aix ], + # TODO should be Chef::Resource::BffPackage + package: [ Chef::Resource::Package, Chef::Provider::Package::Aix ], + rpm_package: [ Chef::Resource::RpmPackage, Chef::Provider::Package::Rpm ], + user: [ Chef::Resource::User, Chef::Provider::User::Aix ], +# service: [ Chef::Resource::AixService, Chef::Provider::Service::Aix ], + + "aix" => { + "aix" => { + "5.6" => { + }, + }, + }, + }, + + "hpux" => { + "hpux" => { + "hpux" => { + "3.1.4" => { + group: [ Chef::Resource::Group, Chef::Provider::Group::Usermod ] + } + } } - - describe "on Ubuntu 14.04" do - let(:os) { "linux" } - let(:platform) { "ubuntu" } - let(:platform_family) { "debian" } - let(:platform_version) { "14.04" } - - supported_providers = [ - :apt_package, :bash, :breakpoint, :chef_gem, :cookbook_file, :csh, :deploy, - :deploy_revision, :directory, :dpkg_package, :easy_install_package, :erl_call, - :execute, :file, :gem_package, :git, :homebrew_package, :http_request, :link, - :log, :macports_package, :pacman_package, :paludis_package, :perl, :python, - :remote_directory, :route, :rpm_package, :ruby, :ruby_block, :script, :subversion, - :template, :timestamped_deploy, :whyrun_safe_ruby_block, :yum_package, - ] - - supported_providers.each do |static_resource| - static_provider = static_mapping[static_resource] - context "when the resource is a #{static_resource}" do - let(:resource) { double(Chef::Resource, provider: nil, resource_name: static_resource) } - let(:action) { :start } # in reality this doesn't matter much - it "should resolve to a #{static_provider} provider" do - expect(provider_resolver).not_to receive(:maybe_chef_platform_lookup) - expect(resolved_provider).to eql(static_provider) + }, + + "netbsd" => { + "netbsd" => { + "netbsd" => { + "3.1.4" => { + group: [ Chef::Resource::Group, Chef::Provider::Group::Groupmod ], + }, + }, + }, + }, + + "openbsd" => { + group: [ Chef::Resource::Group, Chef::Provider::Group::Usermod ], + package: [ Chef::Resource::OpenbsdPackage, Chef::Provider::Package::Openbsd ], + + "openbsd" => { + "openbsd" => { + "3.1.4" => { + }, + }, + }, + }, + + "solaris2" => { + group: [ Chef::Resource::Group, Chef::Provider::Group::Usermod ], + ips_package: [ Chef::Resource::IpsPackage, Chef::Provider::Package::Ips ], + package: [ Chef::Resource::SolarisPackage, Chef::Provider::Package::Solaris ], + mount: [ Chef::Resource::Mount, Chef::Provider::Mount::Solaris ], + solaris_package: [ Chef::Resource::SolarisPackage, Chef::Provider::Package::Solaris ], + + "smartos" => { + smartos_package: [ Chef::Resource::SmartosPackage, Chef::Provider::Package::SmartOS ], + package: [ Chef::Resource::SmartosPackage, Chef::Provider::Package::SmartOS ], + + "smartos" => { + "3.1.4" => { + }, + }, + }, + + "solaris2" => { + "nexentacore" => { + "3.1.4" => { + }, + }, + "omnios" => { + "3.1.4" => { + user: [ Chef::Resource::User, Chef::Provider::User::Solaris ], + } + }, + "openindiana" => { + "3.1.4" => { + }, + }, + "opensolaris" => { + "3.1.4" => { + }, + }, + "solaris2" => { + user: [ Chef::Resource::User, Chef::Provider::User::Solaris ], + "5.11" => { + package: [ Chef::Resource::IpsPackage, Chef::Provider::Package::Ips ], + }, + "5.9" => { + }, + }, + }, + + }, + + "solaris" => { + "solaris" => { + "solaris" => { + "3.1.4" => { + }, + }, + }, + }, + + "exherbo" => { + "exherbo" => { + "exherbo" => { + "3.1.4" => { + # TODO should be Chef::Resource::PaludisPackage + package: [ Chef::Resource::Package, Chef::Provider::Package::Paludis ] + } + } + } + } + } + + def self.create_provider_tests(providers, test, expected, filter) + expected = expected.merge(providers.select { |key, value| key.is_a?(Symbol) }) + providers.each do |key, value| + if !key.is_a?(Symbol) + next_test = test.merge({ filter => key }) + next_filter = + case filter + when :os + :platform_family + when :platform_family + :platform + when :platform + :platform_version + when :platform_version + nil + else + raise "Hash too deep; only os, platform_family, platform and platform_version supported" end - end + create_provider_tests(value, next_test, expected, next_filter) end - - unsupported_providers = [ - :bff_package, :dsc_script, :ips_package, :smartos_package, - :solaris_package, :windows_package, :windows_service, - ] - - unsupported_providers.each do |static_resource| - static_provider = static_mapping[static_resource] - context "when the resource is a #{static_resource}" do - let(:resource) { double(Chef::Resource, provider: nil, resource_name: static_resource) } - let(:action) { :start } # in reality this doesn't matter much - it "should fall back into the old provider mapper code and hooks" do - retval = Object.new - expect(provider_resolver).to receive(:maybe_chef_platform_lookup).and_return(retval) - expect(resolved_provider).to equal(retval) - end - end + end + # If there is no filter, we're as deep as we need to go + if !filter + on_platform test.delete(:platform), test do + expect_providers(expected) end end end + + create_provider_tests(PROVIDERS, {}, {}, :os) end diff --git a/spec/unit/provider_spec.rb b/spec/unit/provider_spec.rb index 5a21b094d0..97b88b1732 100644 --- a/spec/unit/provider_spec.rb +++ b/spec/unit/provider_spec.rb @@ -49,6 +49,13 @@ class ConvergeActionDemonstrator < Chef::Provider end end +class CheckResourceSemanticsDemonstrator < ConvergeActionDemonstrator + def check_resource_semantics! + raise Chef::Exceptions::InvalidResourceSpecification.new("check_resource_semantics!") + end +end + + describe Chef::Provider do before(:each) do @cookbook_collection = Chef::CookbookCollection.new([]) @@ -89,6 +96,10 @@ describe Chef::Provider do expect(@provider.send(:whyrun_supported?)).to eql(false) end + it "should do nothing for check_resource_semantics! by default" do + expect { @provider.check_resource_semantics! }.not_to raise_error + end + it "should return true for action_nothing" do expect(@provider.action_nothing).to eql(true) end @@ -103,9 +114,7 @@ describe Chef::Provider do end it "does not re-load recipes when creating the temporary run context" do - # we actually want to test that RunContext#load is never called, but we - # can't stub all instances of an object with rspec's mocks. :/ - allow(Chef::RunContext).to receive(:new).and_raise("not supposed to happen") + expect_any_instance_of(Chef::RunContext).not_to receive(:load) snitch = Proc.new {temporary_collection = @run_context.resource_collection} @provider.send(:recipe_eval, &snitch) end @@ -176,6 +185,15 @@ describe Chef::Provider do expect(@resource).not_to be_updated_by_last_action end end + + describe "and the resource is invalid" do + let(:provider) { CheckResourceSemanticsDemonstrator.new(@resource, @run_context) } + + it "fails with InvalidResourceSpecification when run" do + expect { provider.run_action(:foo) }.to raise_error(Chef::Exceptions::InvalidResourceSpecification) + end + + end end end diff --git a/spec/unit/recipe_spec.rb b/spec/unit/recipe_spec.rb index 8d0b1bcfd2..ea3ab44c16 100644 --- a/spec/unit/recipe_spec.rb +++ b/spec/unit/recipe_spec.rb @@ -20,6 +20,7 @@ # require 'spec_helper' +require 'chef/platform/resource_priority_map' describe Chef::Recipe do @@ -82,7 +83,7 @@ describe Chef::Recipe do it "should require a name argument" do expect { recipe.cat - }.to raise_error(ArgumentError, "You must supply a name when declaring a cat resource") + }.to raise_error(ArgumentError) end it "should allow regular errors (not NameErrors) to pass unchanged" do @@ -120,7 +121,8 @@ describe Chef::Recipe do it "locate resource for particular platform" do ShaunTheSheep = Class.new(Chef::Resource) - ShaunTheSheep.provides :laughter, :on_platforms => ["television"] + ShaunTheSheep.resource_name :shaun_the_sheep + ShaunTheSheep.provides :laughter, :platform => ["television"] node.automatic[:platform] = "television" node.automatic[:platform_version] = "123" res = recipe.laughter "timmy" @@ -130,12 +132,46 @@ describe Chef::Recipe do it "locate a resource for all platforms" do YourMom = Class.new(Chef::Resource) + YourMom.resource_name :your_mom YourMom.provides :love_and_caring res = recipe.love_and_caring "mommy" expect(res.name).to eql("mommy") res.kind_of?(YourMom) end + describe "when there is more than one resource that resolves on a node" do + before do + node.automatic[:platform] = "nbc_sports" + Sounders = Class.new(Chef::Resource) + Sounders.resource_name :sounders + TottenhamHotspur = Class.new(Chef::Resource) + TottenhamHotspur.resource_name :tottenham_hotspur + end + + after do + Object.send(:remove_const, :Sounders) + Object.send(:remove_const, :TottenhamHotspur) + end + + it "selects the first one alphabetically" do + Sounders.provides :football, platform: "nbc_sports" + TottenhamHotspur.provides :football, platform: "nbc_sports" + + res1 = recipe.football "club world cup" + expect(res1.name).to eql("club world cup") + expect(res1).to be_a_kind_of(Sounders) + end + + it "selects the first one alphabetically even if the declaration order is reversed" do + TottenhamHotspur.provides :football2, platform: "nbc_sports" + Sounders.provides :football2, platform: "nbc_sports" + + res1 = recipe.football2 "club world cup" + expect(res1.name).to eql("club world cup") + expect(res1).to be_a_kind_of(Sounders) + end + end + end end @@ -237,8 +273,17 @@ describe Chef::Recipe do action :nothing end end + + it "validating resources via build_resource" do + expect {recipe.build_resource(:remote_file, "klopp") do + source Chef::DelayedEvaluator.new {"http://chef.io"} + end}.to_not raise_error + end + end + + describe "creating resources via declare_resource" do let(:zm_resource) do recipe.declare_resource(:zen_master, "klopp") do @@ -361,7 +406,7 @@ describe Chef::Recipe do it "does not copy the action from the first resource" do expect(original_resource.action).to eq([:score]) - expect(duplicated_resource.action).to eq(:nothing) + expect(duplicated_resource.action).to eq([:nothing]) end it "does not copy the source location of the first resource" do @@ -529,6 +574,36 @@ describe Chef::Recipe do expect(cookbook_collection[:openldap]).not_to receive(:load_recipe).with("default", run_context) openldap_recipe.include_recipe "::default" end + + it "will not load a recipe twice when called first from an LWRP provider" do + openldap_recipe = Chef::Recipe.new("openldap", "test", run_context) + expect(node).to receive(:loaded_recipe).with(:openldap, "default").exactly(:once) + allow(run_context).to receive(:unreachable_cookbook?).with(:openldap).and_return(false) + expect(cookbook_collection[:openldap]).to receive(:load_recipe).with("default", run_context) + openldap_recipe.include_recipe "::default" + expect(cookbook_collection[:openldap]).not_to receive(:load_recipe).with("default", run_context) + openldap_recipe.openldap_includer("do it").run_action(:run) + end + + it "will not load a recipe twice when called last from an LWRP provider" do + openldap_recipe = Chef::Recipe.new("openldap", "test", run_context) + expect(node).to receive(:loaded_recipe).with(:openldap, "default").exactly(:once) + allow(run_context).to receive(:unreachable_cookbook?).with(:openldap).and_return(false) + expect(cookbook_collection[:openldap]).to receive(:load_recipe).with("default", run_context) + openldap_recipe.openldap_includer("do it").run_action(:run) + expect(cookbook_collection[:openldap]).not_to receive(:load_recipe).with("default", run_context) + openldap_recipe.include_recipe "::default" + end + + it "will not load a recipe twice when called both times from an LWRP provider" do + openldap_recipe = Chef::Recipe.new("openldap", "test", run_context) + expect(node).to receive(:loaded_recipe).with(:openldap, "default").exactly(:once) + allow(run_context).to receive(:unreachable_cookbook?).with(:openldap).and_return(false) + expect(cookbook_collection[:openldap]).to receive(:load_recipe).with("default", run_context) + openldap_recipe.openldap_includer("do it").run_action(:run) + expect(cookbook_collection[:openldap]).not_to receive(:load_recipe).with("default", run_context) + openldap_recipe.openldap_includer("do it").run_action(:run) + end end describe "tags" do @@ -593,5 +668,9 @@ describe Chef::Recipe do expect(recipe.singleton_class.included_modules).to include(Chef::DSL::Audit) expect(recipe.respond_to?(:control_group)).to be true end + + it "should respond to :ps_credential from Chef::DSL::Powershell" do + expect(recipe.respond_to?(:ps_credential)).to be true + end end end diff --git a/spec/unit/registry_helper_spec.rb b/spec/unit/registry_helper_spec.rb index 036a0834db..b2d0b7b125 100644 --- a/spec/unit/registry_helper_spec.rb +++ b/spec/unit/registry_helper_spec.rb @@ -21,6 +21,7 @@ require 'spec_helper' describe Chef::Provider::RegistryKey do let(:value1) { { :name => "one", :type => :string, :data => "1" } } + let(:value1_upcase_name) { {:name => "ONE", :type => :string, :data => "1"} } let(:key_path) { 'HKCU\Software\OpscodeNumbers' } let(:key) { 'Software\OpscodeNumbers' } let(:key_parent) { 'Software' } @@ -71,7 +72,20 @@ describe Chef::Provider::RegistryKey do expect(@registry).to receive(:data_exists?).with(key_path, value1).and_return(true) @registry.set_value(key_path, value1) end - + it "does nothing if case insensitive key and hive and value exist" do + expect(@registry).to receive(:key_exists!).with(key_path.downcase).and_return(true) + expect(@registry).to receive(:get_hive_and_key).with(key_path.downcase).and_return([@hive_mock, key]) + expect(@registry).to receive(:value_exists?).with(key_path.downcase, value1).and_return(true) + expect(@registry).to receive(:data_exists?).with(key_path.downcase, value1).and_return(true) + @registry.set_value(key_path.downcase, value1) + end + it "does nothing if key and hive and value with a case insensitive name exist" do + expect(@registry).to receive(:key_exists!).with(key_path.downcase).and_return(true) + expect(@registry).to receive(:get_hive_and_key).with(key_path.downcase).and_return([@hive_mock, key]) + expect(@registry).to receive(:value_exists?).with(key_path.downcase, value1_upcase_name).and_return(true) + expect(@registry).to receive(:data_exists?).with(key_path.downcase, value1_upcase_name).and_return(true) + @registry.set_value(key_path.downcase, value1_upcase_name) + end it "updates value if key and hive and value exist, but data is different" do expect(@registry).to receive(:key_exists!).with(key_path).and_return(true) expect(@registry).to receive(:get_hive_and_key).with(key_path).and_return([@hive_mock, key]) diff --git a/spec/unit/resource/batch_spec.rb b/spec/unit/resource/batch_spec.rb index 4a056b8735..b8c2897f42 100644 --- a/spec/unit/resource/batch_spec.rb +++ b/spec/unit/resource/batch_spec.rb @@ -25,6 +25,7 @@ describe Chef::Resource::Batch do node.default["kernel"] = Hash.new node.default["kernel"][:machine] = :x86_64.to_s + node.automatic[:os] = 'windows' run_context = Chef::RunContext.new(node, nil, nil) diff --git a/spec/unit/resource/breakpoint_spec.rb b/spec/unit/resource/breakpoint_spec.rb index ed1f3ebcd5..88ab34d568 100644 --- a/spec/unit/resource/breakpoint_spec.rb +++ b/spec/unit/resource/breakpoint_spec.rb @@ -37,7 +37,7 @@ describe Chef::Resource::Breakpoint do end it "defaults to the break action" do - expect(@breakpoint.action).to eq("break") + expect(@breakpoint.action).to eq([:break]) end it "names itself after the line number of the file where it's created" do diff --git a/spec/unit/resource/cron_spec.rb b/spec/unit/resource/cron_spec.rb index 743552c1de..0978be6930 100644 --- a/spec/unit/resource/cron_spec.rb +++ b/spec/unit/resource/cron_spec.rb @@ -35,7 +35,7 @@ describe Chef::Resource::Cron do end it "should have a default action of 'create'" do - expect(@resource.action).to eql(:create) + expect(@resource.action).to eql([:create]) end it "should accept create or delete for action" do diff --git a/spec/unit/resource/deploy_spec.rb b/spec/unit/resource/deploy_spec.rb index 07f5f973c0..5b6a452784 100644 --- a/spec/unit/resource/deploy_spec.rb +++ b/spec/unit/resource/deploy_spec.rb @@ -30,35 +30,12 @@ describe Chef::Resource::Deploy do class << self - - def resource_has_a_hash_attribute(attr_name) - it "has a Hash attribute for #{attr_name.to_s}" do - @resource.send(attr_name, {foo: "bar"}) - expect(@resource.send(attr_name)).to eql({foo: "bar"}) - expect {@resource.send(attr_name, 8675309)}.to raise_error(ArgumentError) - end - - it "the Hash attribute for #{attr_name.to_s} is nillable" do - @resource.send(attr_name, {foo: "bar"}) - expect(@resource.send(attr_name)).to eql({foo: "bar"}) - @resource.send(attr_name, nil) - expect(@resource.send(attr_name)).to eql(nil) - end - end - def resource_has_a_string_attribute(attr_name) it "has a String attribute for #{attr_name.to_s}" do @resource.send(attr_name, "this is a string") expect(@resource.send(attr_name)).to eql("this is a string") expect {@resource.send(attr_name, 8675309)}.to raise_error(ArgumentError) end - - it "the String attribute for #{attr_name.to_s} is nillable" do - @resource.send(attr_name, "this is a string") - expect(@resource.send(attr_name)).to eql("this is a string") - @resource.send(attr_name, nil) - expect(@resource.send(attr_name)).to eql(nil) - end end def resource_has_a_boolean_attribute(attr_name, opts={:defaults_to=>false}) @@ -171,10 +148,16 @@ describe Chef::Resource::Deploy do expect(@resource.current_path).to eql("/my/deploy/dir/current") end + it "allows depth to be set via integer" do + expect(@resource.depth).to be_nil + @resource.depth 1 + expect(@resource.depth).to eql(1) + end + it "gives #depth as 5 if shallow clone is true, nil otherwise" do expect(@resource.depth).to be_nil @resource.shallow_clone true - expect(@resource.depth).to eql("5") + expect(@resource.depth).to eql(5) end it "aliases repo as repository" do @@ -212,10 +195,6 @@ describe Chef::Resource::Deploy do expect(@resource.symlink_before_migrate).to eq({"wtf?" => "wtf is going on"}) end - resource_has_a_hash_attribute :symlink_before_migrate - resource_has_a_hash_attribute :symlinks - resource_has_a_hash_attribute :additional_remotes - resource_has_a_callback_attribute :before_migrate resource_has_a_callback_attribute :before_symlink resource_has_a_callback_attribute :before_restart diff --git a/spec/unit/resource/directory_spec.rb b/spec/unit/resource/directory_spec.rb index c452b2a914..e9e80806db 100644 --- a/spec/unit/resource/directory_spec.rb +++ b/spec/unit/resource/directory_spec.rb @@ -35,7 +35,7 @@ describe Chef::Resource::Directory do end it "should have a default action of 'create'" do - expect(@resource.action).to eql(:create) + expect(@resource.action).to eql([:create]) end it "should accept create or delete for action" do diff --git a/spec/unit/resource/dsc_resource_spec.rb b/spec/unit/resource/dsc_resource_spec.rb new file mode 100644 index 0000000000..06769d86ce --- /dev/null +++ b/spec/unit/resource/dsc_resource_spec.rb @@ -0,0 +1,85 @@ +# +# Author:: Adam Edwards (<adamed@getchef.com>) +# 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::DscResource do + let(:dsc_test_resource_name) { 'DSCTest' } + let(:dsc_test_property_name) { :DSCTestProperty } + let(:dsc_test_property_value) { 'DSCTestValue' } + + context 'when Powershell supports Dsc' do + let(:dsc_test_run_context) { + node = Chef::Node.new + node.automatic[:languages][:powershell][:version] = '5.0.10018.0' + empty_events = Chef::EventDispatch::Dispatcher.new + Chef::RunContext.new(node, {}, empty_events) + } + let(:dsc_test_resource) { + Chef::Resource::DscResource.new(dsc_test_resource_name, dsc_test_run_context) + } + + it "has a default action of `:run`" do + expect(dsc_test_resource.action).to eq([:run]) + end + + it "has an allowed_actions attribute with only the `:run` and `:nothing` attributes" do + expect(dsc_test_resource.allowed_actions.to_set).to eq([:run,:nothing].to_set) + end + + it "allows the resource attribute to be set" do + dsc_test_resource.resource(dsc_test_resource_name) + expect(dsc_test_resource.resource).to eq(dsc_test_resource_name) + end + + it "allows the module_name attribute to be set" do + dsc_test_resource.module_name(dsc_test_resource_name) + expect(dsc_test_resource.module_name).to eq(dsc_test_resource_name) + end + + context "when setting a dsc property" do + it "allows setting a dsc property with a property name of type Symbol" do + dsc_test_resource.property(dsc_test_property_name, dsc_test_property_value) + expect(dsc_test_resource.property(dsc_test_property_name)).to eq(dsc_test_property_value) + expect(dsc_test_resource.properties[dsc_test_property_name]).to eq(dsc_test_property_value) + end + + it "raises a TypeError if property_name is not a symbol" do + expect{ + dsc_test_resource.property('Foo', dsc_test_property_value) + }.to raise_error(TypeError) + end + + context "when using DelayedEvaluators" do + it "allows setting a dsc property with a property name of type Symbol" do + dsc_test_resource.property(dsc_test_property_name, Chef::DelayedEvaluator.new { + dsc_test_property_value + }) + expect(dsc_test_resource.property(dsc_test_property_name)).to eq(dsc_test_property_value) + expect(dsc_test_resource.properties[dsc_test_property_name]).to eq(dsc_test_property_value) + end + end + end + + context 'Powershell DSL methods' do + it "responds to :ps_credential" do + expect(dsc_test_resource.respond_to?(:ps_credential)).to be true + end + end + end +end diff --git a/spec/unit/resource/dsc_script_spec.rb b/spec/unit/resource/dsc_script_spec.rb index 71103ea590..1fa865a2d5 100644 --- a/spec/unit/resource/dsc_script_spec.rb +++ b/spec/unit/resource/dsc_script_spec.rb @@ -29,7 +29,7 @@ describe Chef::Resource::DscScript do Chef::RunContext.new(node, {}, empty_events) } let(:dsc_test_resource) { - Chef::Resource::DscScript.new(dsc_test_resource_name, dsc_test_run_context) + Chef::Resource::DscScript.new(dsc_test_resource_name, dsc_test_run_context) } let(:configuration_code) {'echo "This is supposed to create a configuration document."'} let(:configuration_path) {'c:/myconfigs/formatc.ps1'} @@ -38,7 +38,7 @@ describe Chef::Resource::DscScript do let(:configuration_data_script) { 'c:/myconfigs/data/safedata.psd1' } it "has a default action of `:run`" do - expect(dsc_test_resource.action).to eq(:run) + expect(dsc_test_resource.action).to eq([:run]) end it "has an allowed_actions attribute with only the `:run` and `:nothing` attributes" do @@ -70,6 +70,10 @@ describe Chef::Resource::DscScript do expect(dsc_test_resource.configuration_data_script).to eq(configuration_data_script) end + it "has the ps_credential helper method" do + expect(dsc_test_resource).to respond_to(:ps_credential) + end + context "when calling imports" do let(:module_name) { 'FooModule' } let(:module_name_b) { 'BarModule' } diff --git a/spec/unit/resource/env_spec.rb b/spec/unit/resource/env_spec.rb index 566827a27e..9bee07c593 100644 --- a/spec/unit/resource/env_spec.rb +++ b/spec/unit/resource/env_spec.rb @@ -35,7 +35,7 @@ describe Chef::Resource::Env do end it "should have a default action of 'create'" do - expect(@resource.action).to eql(:create) + expect(@resource.action).to eql([:create]) end { :create => false, :delete => false, :modify => false, :flibber => true }.each do |action,bad_value| diff --git a/spec/unit/resource/erl_call_spec.rb b/spec/unit/resource/erl_call_spec.rb index 8ec182665f..9abf2e7812 100644 --- a/spec/unit/resource/erl_call_spec.rb +++ b/spec/unit/resource/erl_call_spec.rb @@ -35,7 +35,7 @@ describe Chef::Resource::ErlCall do end it "should have a default action of run" do - expect(@resource.action).to eql("run") + expect(@resource.action).to eql([:run]) end it "should accept run as an action" do diff --git a/spec/unit/resource/file/verification_spec.rb b/spec/unit/resource/file/verification_spec.rb index 3609d9d482..6b929789c8 100644 --- a/spec/unit/resource/file/verification_spec.rb +++ b/spec/unit/resource/file/verification_spec.rb @@ -69,12 +69,40 @@ describe Chef::Resource::File::Verification do end context "with a verification command(String)" do + before(:each) do + allow(Chef::Log).to receive(:deprecation).and_return(nil) + end + + def platform_specific_verify_command(variable_name) + if windows? + "if \"#{temp_path}\" == \"%{#{variable_name}}\" (exit 0) else (exit 1)" + else + "test #{temp_path} = %{#{variable_name}}" + end + end + it "substitutes \%{file} with the path" do - test_command = if windows? - "if \"#{temp_path}\" == \"%{file}\" (exit 0) else (exit 1)" - else - "test #{temp_path} = %{file}" - end + test_command = platform_specific_verify_command('file') + v = Chef::Resource::File::Verification.new(parent_resource, test_command, {}) + expect(v.verify(temp_path)).to eq(true) + end + + it "warns about deprecation when \%{file} is used" do + expect(Chef::Log).to receive(:deprecation).with(/%{file} is deprecated/, /verification_spec\.rb/) + test_command = platform_specific_verify_command('file') + Chef::Resource::File::Verification.new(parent_resource, test_command, {}) + .verify(temp_path) + end + + it "does not warn about deprecation when \%{file} is not used" do + expect(Chef::Log).to_not receive(:deprecation) + test_command = platform_specific_verify_command('path') + Chef::Resource::File::Verification.new(parent_resource, test_command, {}) + .verify(temp_path) + end + + it "substitutes \%{path} with the path" do + test_command = platform_specific_verify_command('path') v = Chef::Resource::File::Verification.new(parent_resource, test_command, {}) expect(v.verify(temp_path)).to eq(true) end diff --git a/spec/unit/resource/file_spec.rb b/spec/unit/resource/file_spec.rb index db52e35004..76beaf15e1 100644 --- a/spec/unit/resource/file_spec.rb +++ b/spec/unit/resource/file_spec.rb @@ -29,7 +29,7 @@ describe Chef::Resource::File do end it "should have a default action of 'create'" do - expect(@resource.action).to eql("create") + expect(@resource.action).to eql([:create]) end it "should have a default content of nil" do diff --git a/spec/unit/resource/group_spec.rb b/spec/unit/resource/group_spec.rb index bcf9205f7e..a4029fc911 100644 --- a/spec/unit/resource/group_spec.rb +++ b/spec/unit/resource/group_spec.rb @@ -50,7 +50,7 @@ describe Chef::Resource::Group, "initialize" do end it "should set action to :create" do - expect(@resource.action).to eql(:create) + expect(@resource.action).to eql([:create]) end %w{create remove modify manage}.each do |action| diff --git a/spec/unit/resource/ifconfig_spec.rb b/spec/unit/resource/ifconfig_spec.rb index ea5282acd5..e3e1f6daa2 100644 --- a/spec/unit/resource/ifconfig_spec.rb +++ b/spec/unit/resource/ifconfig_spec.rb @@ -47,21 +47,23 @@ describe Chef::Resource::Ifconfig do end end - shared_examples "being a platform using the default ifconfig provider" do |platform, version| + shared_examples "being a platform based on an old Debian" do |platform, version| before do + @node.automatic_attrs[:os] = 'linux' + @node.automatic_attrs[:platform_family] = 'debian' @node.automatic_attrs[:platform] = platform @node.automatic_attrs[:platform_version] = version end it "should use an ordinary Provider::Ifconfig as a provider for #{platform} #{version}" do - expect(@resource.provider_for_action(:add)).to be_a_kind_of(Chef::Provider::Ifconfig) - expect(@resource.provider_for_action(:add)).not_to be_a_kind_of(Chef::Provider::Ifconfig::Debian) - expect(@resource.provider_for_action(:add)).not_to be_a_kind_of(Chef::Provider::Ifconfig::Redhat) + expect(@resource.provider_for_action(:add).class).to eq(Chef::Provider::Ifconfig) end end shared_examples "being a platform based on RedHat" do |platform, version| before do + @node.automatic_attrs[:os] = 'linux' + @node.automatic_attrs[:platform_family] = 'rhel' @node.automatic_attrs[:platform] = platform @node.automatic_attrs[:platform_version] = version end @@ -73,6 +75,8 @@ describe Chef::Resource::Ifconfig do shared_examples "being a platform based on a recent Debian" do |platform, version| before do + @node.automatic_attrs[:os] = 'linux' + @node.automatic_attrs[:platform_family] = 'debian' @node.automatic_attrs[:platform] = platform @node.automatic_attrs[:platform_version] = version end @@ -87,7 +91,7 @@ describe Chef::Resource::Ifconfig do end describe "when it is an old Debian platform" do - it_should_behave_like "being a platform using the default ifconfig provider", "debian", "6.0" + it_should_behave_like "being a platform based on an old Debian", "debian", "6.0" end describe "when it is a new Debian platform" do @@ -95,7 +99,7 @@ describe Chef::Resource::Ifconfig do end describe "when it is an old Ubuntu platform" do - it_should_behave_like "being a platform using the default ifconfig provider", "ubuntu", "11.04" + it_should_behave_like "being a platform based on an old Debian", "ubuntu", "11.04" end describe "when it is a new Ubuntu platform" do diff --git a/spec/unit/resource/link_spec.rb b/spec/unit/resource/link_spec.rb index 3573a15f31..0246fcd13b 100644 --- a/spec/unit/resource/link_spec.rb +++ b/spec/unit/resource/link_spec.rb @@ -36,7 +36,7 @@ describe Chef::Resource::Link do end it "should have a default action of 'create'" do - expect(@resource.action).to eql(:create) + expect(@resource.action).to eql([:create]) end { :create => false, :delete => false, :blues => true }.each do |action,bad_value| @@ -53,6 +53,21 @@ describe Chef::Resource::Link do expect(@resource.target_file).to eql("fakey_fakerton") end + it "should accept a delayed evaluator as the target path" do + @resource.target_file Chef::DelayedEvaluator.new { "my_lazy_name" } + expect(@resource.target_file).to eql("my_lazy_name") + end + + it "should accept a delayed evaluator when accessing via 'path'" do + @resource.target_file Chef::DelayedEvaluator.new { "my_lazy_name" } + expect(@resource.path).to eql("my_lazy_name") + end + + it "should accept a delayed evaluator via 'to'" do + @resource.to Chef::DelayedEvaluator.new { "my_lazy_name" } + expect(@resource.to).to eql("my_lazy_name") + end + it "should accept a string as the link source via 'to'" do expect { @resource.to "/tmp" }.not_to raise_error end diff --git a/spec/unit/resource/mdadm_spec.rb b/spec/unit/resource/mdadm_spec.rb index 866309ec5b..6ca99c58e5 100644 --- a/spec/unit/resource/mdadm_spec.rb +++ b/spec/unit/resource/mdadm_spec.rb @@ -35,7 +35,7 @@ describe Chef::Resource::Mdadm do end it "should have a default action of create" do - expect(@resource.action).to eql(:create) + expect(@resource.action).to eql([:create]) end it "should accept create, assemble, stop as actions" do diff --git a/spec/unit/resource/mount_spec.rb b/spec/unit/resource/mount_spec.rb index ad95c06e04..acce26dcab 100644 --- a/spec/unit/resource/mount_spec.rb +++ b/spec/unit/resource/mount_spec.rb @@ -38,7 +38,7 @@ describe Chef::Resource::Mount do end it "should have a default action of mount" do - expect(@resource.action).to eql(:mount) + expect(@resource.action).to eql([:mount]) end it "should accept mount, umount and remount as actions" do diff --git a/spec/unit/resource/ohai_spec.rb b/spec/unit/resource/ohai_spec.rb index fe29755abf..3bc21a41d2 100644 --- a/spec/unit/resource/ohai_spec.rb +++ b/spec/unit/resource/ohai_spec.rb @@ -34,7 +34,7 @@ describe Chef::Resource::Ohai do end it "should have a default action of create" do - expect(@resource.action).to eql(:reload) + expect(@resource.action).to eql([:reload]) end it "should allow you to set the plugin attribute" do diff --git a/spec/unit/resource/powershell_spec.rb b/spec/unit/resource/powershell_script_spec.rb index c263172ae6..2505c4a3d7 100644 --- a/spec/unit/resource/powershell_spec.rb +++ b/spec/unit/resource/powershell_script_spec.rb @@ -25,6 +25,7 @@ describe Chef::Resource::PowershellScript do node.default["kernel"] = Hash.new node.default["kernel"][:machine] = :x86_64.to_s + node.automatic[:os] = 'windows' run_context = Chef::RunContext.new(node, nil, nil) diff --git a/spec/unit/resource/registry_key_spec.rb b/spec/unit/resource/registry_key_spec.rb index e2a864d73a..2e2811d026 100644 --- a/spec/unit/resource/registry_key_spec.rb +++ b/spec/unit/resource/registry_key_spec.rb @@ -45,7 +45,7 @@ describe Chef::Resource::RegistryKey, "initialize" do end it "should set action to :create" do - expect(@resource.action).to eql(:create) + expect(@resource.action).to eql([:create]) end %w{create create_if_missing delete delete_key}.each do |action| diff --git a/spec/unit/resource/remote_file_spec.rb b/spec/unit/resource/remote_file_spec.rb index 3731d1aee2..0a379ff574 100644 --- a/spec/unit/resource/remote_file_spec.rb +++ b/spec/unit/resource/remote_file_spec.rb @@ -39,6 +39,11 @@ describe Chef::Resource::RemoteFile do expect(Chef::Platform.find_provider(:noplatform, 'noversion', @resource)).to eq(Chef::Provider::RemoteFile) end + it "says its provider is RemoteFile when the source is a network share" do + @resource.source("\\\\fakey\\fakerton\\fake.txt") + expect(@resource.provider).to eq(Chef::Provider::RemoteFile) + expect(Chef::Platform.find_provider(:noplatform, 'noversion', @resource)).to eq(Chef::Provider::RemoteFile) + end describe "source" do it "does not have a default value for 'source'" do @@ -50,6 +55,16 @@ describe Chef::Resource::RemoteFile do expect(@resource.source).to eql([ "http://opscode.com/" ]) end + it "should accept a windows network share source" do + @resource.source "\\\\fakey\\fakerton\\fake.txt" + expect(@resource.source).to eql([ "\\\\fakey\\fakerton\\fake.txt" ]) + end + + it 'should accept file URIs with spaces' do + @resource.source("file:///C:/foo bar") + expect(@resource.source).to eql(["file:///C:/foo bar"]) + end + it "should accept a delayed evalutator (string) for the remote file source" do @resource.source Chef::DelayedEvaluator.new {"http://opscode.com/"} expect(@resource.source).to eql([ "http://opscode.com/" ]) diff --git a/spec/unit/resource/ruby_block_spec.rb b/spec/unit/resource/ruby_block_spec.rb index 9f19fecd4f..8664564ac5 100644 --- a/spec/unit/resource/ruby_block_spec.rb +++ b/spec/unit/resource/ruby_block_spec.rb @@ -30,8 +30,8 @@ describe Chef::Resource::RubyBlock do expect(@resource).to be_a_kind_of(Chef::Resource::RubyBlock) end - it "should have a default action of 'create'" do - expect(@resource.action).to eql("run") + it "should have a default action of 'run'" do + expect(@resource.action).to eql([:run]) end it "should have a resource name of :ruby_block" do diff --git a/spec/unit/resource/service_spec.rb b/spec/unit/resource/service_spec.rb index eb6f444e93..b9e3757255 100644 --- a/spec/unit/resource/service_spec.rb +++ b/spec/unit/resource/service_spec.rb @@ -1,7 +1,7 @@ # # Author:: AJ Christensen (<aj@hjksolutions.com>) # Author:: Tyler Cloke (<tyler@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Copyright:: Copyright (c) 2008-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -139,14 +139,14 @@ describe Chef::Resource::Service do expect { @resource.send(attrib, "poop") }.to raise_error(ArgumentError) end - it "should default all the feature support to false" do - support_hash = { :status => false, :restart => false, :reload=> false } + it "should default all the feature support to nil" do + support_hash = { :status => nil, :restart => nil, :reload=> nil } expect(@resource.supports).to eq(support_hash) end it "should allow you to set what features this resource supports as a array" do support_array = [ :status, :restart ] - support_hash = { :status => true, :restart => true, :reload => false } + support_hash = { :status => true, :restart => true, :reload => nil } @resource.supports(support_array) expect(@resource.supports).to eq(support_hash) end diff --git a/spec/unit/resource/template_spec.rb b/spec/unit/resource/template_spec.rb index df5ca94b8a..2fd951b72d 100644 --- a/spec/unit/resource/template_spec.rb +++ b/spec/unit/resource/template_spec.rb @@ -98,7 +98,7 @@ describe Chef::Resource::Template do context "on windows", :windows_only do # according to Chef::Resource::File, windows state attributes are rights + deny_rights - pending "it describes its state" + skip "it describes its state" end it "returns the file path as its identity" do diff --git a/spec/unit/resource/timestamped_deploy_spec.rb b/spec/unit/resource/timestamped_deploy_spec.rb index eca6c570d4..4ebfdaf059 100644 --- a/spec/unit/resource/timestamped_deploy_spec.rb +++ b/spec/unit/resource/timestamped_deploy_spec.rb @@ -23,11 +23,10 @@ describe Chef::Resource::TimestampedDeploy, "initialize" do static_provider_resolution( resource: Chef::Resource::TimestampedDeploy, provider: Chef::Provider::Deploy::Timestamped, - name: :deploy, + name: :timestamped_deploy, action: :deploy, os: 'linux', platform_family: 'rhel', ) end - diff --git a/spec/unit/resource/user_spec.rb b/spec/unit/resource/user_spec.rb index f05de94fe0..3bf7e6187b 100644 --- a/spec/unit/resource/user_spec.rb +++ b/spec/unit/resource/user_spec.rb @@ -43,7 +43,7 @@ describe Chef::Resource::User, "initialize" do end it "should set action to :create" do - expect(@resource.action).to eql(:create) + expect(@resource.action).to eql([:create]) end it "should set supports[:manage_home] to false" do diff --git a/spec/unit/resource/windows_package_spec.rb b/spec/unit/resource/windows_package_spec.rb index 1e02f2449b..6aa5d357ea 100644 --- a/spec/unit/resource/windows_package_spec.rb +++ b/spec/unit/resource/windows_package_spec.rb @@ -63,9 +63,9 @@ describe Chef::Resource::WindowsPackage, "initialize" do end it "coverts a source to an absolute path" do - allow(::File).to receive(:absolute_path).and_return("c:\\Files\\frost.msi") + allow(::File).to receive(:absolute_path).and_return("c:\\files\\frost.msi") resource.source("frost.msi") - expect(resource.source).to eql "c:\\Files\\frost.msi" + expect(resource.source).to eql "c:\\files\\frost.msi" end it "converts slashes to backslashes in the source path" do @@ -78,4 +78,18 @@ describe Chef::Resource::WindowsPackage, "initialize" do # it's a little late to stub out File.absolute_path expect(resource.source).to include("solitaire.msi") end + + it "supports the checksum attribute" do + resource.checksum('somechecksum') + expect(resource.checksum).to eq('somechecksum') + end + + context 'when a URL is used' do + let(:resource_source) { 'https://foo.bar/solitare.msi' } + let(:resource) { Chef::Resource::WindowsPackage.new(resource_source) } + + it "should return the source unmodified" do + expect(resource.source).to eq(resource_source) + end + end end diff --git a/spec/unit/resource/yum_package_spec.rb b/spec/unit/resource/yum_package_spec.rb index e01b87c580..f24f1e3405 100644 --- a/spec/unit/resource/yum_package_spec.rb +++ b/spec/unit/resource/yum_package_spec.rb @@ -1,6 +1,6 @@ # # Author:: AJ Christensen (<aj@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Copyright:: Copyright (c) 2008-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -78,3 +78,12 @@ describe Chef::Resource::YumPackage, "allow_downgrade" do expect { @resource.allow_downgrade "monkey" }.to raise_error(ArgumentError) end end + +describe Chef::Resource::YumPackage, "yum_binary" do + let(:resource) { Chef::Resource::YumPackage.new("foo") } + + it "should allow you to specify the yum_binary" do + resource.yum_binary "/usr/bin/yum-something" + expect(resource.yum_binary).to eql("/usr/bin/yum-something") + end +end diff --git a/spec/unit/resource_collection_spec.rb b/spec/unit/resource_collection_spec.rb index b43b012dfc..d52e7e2c26 100644 --- a/spec/unit/resource_collection_spec.rb +++ b/spec/unit/resource_collection_spec.rb @@ -252,7 +252,7 @@ describe Chef::ResourceCollection do expect(json).to match(/instance_vars/) end - include_examples "to_json equalivent to Chef::JSONCompat.to_json" do + include_examples "to_json equivalent to Chef::JSONCompat.to_json" do let(:jsonable) { rc } end end diff --git a/spec/unit/resource_resolver_spec.rb b/spec/unit/resource_resolver_spec.rb new file mode 100644 index 0000000000..b3bda9d945 --- /dev/null +++ b/spec/unit/resource_resolver_spec.rb @@ -0,0 +1,53 @@ +# +# Author:: Ranjib Dey +# Copyright:: Copyright (c) 2015 Ranjib Dey <ranjib@linux.com>. +# 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/resource_resolver' + + +describe Chef::ResourceResolver do + it '#resolve' do + expect(described_class.resolve(:execute)).to eq(Chef::Resource::Execute) + end + + it '#list' do + expect(described_class.list(:package)).to_not be_empty + end + + context 'instance methods' do + let(:resolver) do + described_class.new(Chef::Node.new, 'execute') + end + + it '#resolve' do + expect(resolver.resolve).to eq Chef::Resource::Execute + end + + it '#list' do + expect(resolver.list).to eq [ Chef::Resource::Execute ] + end + + it '#provided_by? returns true when resource class is in the list' do + expect(resolver.provided_by?(Chef::Resource::Execute)).to be_truthy + end + + it '#provided_by? returns false when resource class is not in the list' do + expect(resolver.provided_by?(Chef::Resource::File)).to be_falsey + end + end +end diff --git a/spec/unit/resource_spec.rb b/spec/unit/resource_spec.rb index 8214021f65..b9ba80068b 100644 --- a/spec/unit/resource_spec.rb +++ b/spec/unit/resource_spec.rb @@ -21,10 +21,6 @@ require 'spec_helper' -class ResourceTestHarness < Chef::Resource - provider_base Chef::Provider::Package -end - describe Chef::Resource do before(:each) do @cookbook_repo_path = File.join(CHEF_SPEC_DATA, 'cookbooks') @@ -35,6 +31,18 @@ describe Chef::Resource do @resource = Chef::Resource.new("funk", @run_context) end + it "should mixin shell_out" do + expect(@resource.respond_to?(:shell_out)).to be true + end + + it "should mixin shell_out!" do + expect(@resource.respond_to?(:shell_out!)).to be true + end + + it "should mixin shell_out_with_systems_locale" do + expect(@resource.respond_to?(:shell_out_with_systems_locale)).to be true + end + describe "when inherited" do it "adds an entry to a list of subclasses" do @@ -51,8 +59,8 @@ describe Chef::Resource do end describe "when declaring the identity attribute" do - it "has no identity attribute by default" do - expect(Chef::Resource.identity_attr).to be_nil + it "has :name as identity attribute by default" do + expect(Chef::Resource.identity_attr).to eq(:name) end it "sets an identity attribute" do @@ -324,6 +332,86 @@ describe Chef::Resource do end end + describe "self.resource_name" do + context "When resource_name is not set" do + it "and there are no provides lines, resource_name is nil" do + c = Class.new(Chef::Resource) do + end + + r = c.new('hi') + r.declared_type = :d + expect(c.resource_name).to be_nil + expect(r.resource_name).to be_nil + expect(r.declared_type).to eq :d + end + + it "and there are no provides lines, @resource_name is used" do + c = Class.new(Chef::Resource) do + def initialize(*args, &block) + @resource_name = :blah + super + end + end + + r = c.new('hi') + r.declared_type = :d + expect(c.resource_name).to be_nil + expect(r.resource_name).to eq :blah + expect(r.declared_type).to eq :d + end + + it "and the resource class gets a late-bound name, resource_name is nil" do + c = Class.new(Chef::Resource) do + def self.name + "ResourceSpecNameTest" + end + end + + r = c.new('hi') + r.declared_type = :d + expect(c.resource_name).to be_nil + expect(r.resource_name).to be_nil + expect(r.declared_type).to eq :d + end + end + + it "resource_name without provides is honored" do + c = Class.new(Chef::Resource) do + resource_name 'blah' + end + + r = c.new('hi') + r.declared_type = :d + expect(c.resource_name).to eq :blah + expect(r.resource_name).to eq :blah + expect(r.declared_type).to eq :d + end + it "setting class.resource_name with 'resource_name = blah' overrides declared_type" do + c = Class.new(Chef::Resource) do + provides :self_resource_name_test_2 + end + c.resource_name = :blah + + r = c.new('hi') + r.declared_type = :d + expect(c.resource_name).to eq :blah + expect(r.resource_name).to eq :blah + expect(r.declared_type).to eq :d + end + it "setting class.resource_name with 'resource_name blah' overrides declared_type" do + c = Class.new(Chef::Resource) do + resource_name :blah + provides :self_resource_name_test_3 + end + + r = c.new('hi') + r.declared_type = :d + expect(c.resource_name).to eq :blah + expect(r.resource_name).to eq :blah + expect(r.declared_type).to eq :d + end + end + describe "is" do it "should return the arguments passed with 'is'" do zm = Chef::Resource::ZenMaster.new("coffee") @@ -343,7 +431,7 @@ describe Chef::Resource do expect(json).to match(/instance_vars/) end - include_examples "to_json equalivent to Chef::JSONCompat.to_json" do + include_examples "to_json equivalent to Chef::JSONCompat.to_json" do let(:jsonable) { @resource } end end @@ -447,8 +535,21 @@ describe Chef::Resource do expect(Chef::Resource.provider_base).to eq(Chef::Provider) end - it "allows the base provider to be overriden by a " do - expect(ResourceTestHarness.provider_base).to eq(Chef::Provider::Package) + it "allows the base provider to be overridden" do + Chef::Config.treat_deprecation_warnings_as_errors(false) + class OverrideProviderBaseTest < Chef::Resource + provider_base Chef::Provider::Package + end + + expect(OverrideProviderBaseTest.provider_base).to eq(Chef::Provider::Package) + end + + it "warns when setting provider_base" do + expect { + class OverrideProviderBaseTest2 < Chef::Resource + provider_base Chef::Provider::Package + end + }.to raise_error(Chef::Exceptions::DeprecatedFeatureError) end end @@ -709,21 +810,21 @@ describe Chef::Resource do end it 'adds mappings for a single platform' do - expect(Chef::Resource.node_map).to receive(:set).with( + expect(Chef.resource_handler_map).to receive(:set).with( :dinobot, Chef::Resource::Klz, { platform: ['autobots'] } ) klz.provides :dinobot, platform: ['autobots'] end it 'adds mappings for multiple platforms' do - expect(Chef::Resource.node_map).to receive(:set).with( + expect(Chef.resource_handler_map).to receive(:set).with( :energy, Chef::Resource::Klz, { platform: ['autobots', 'decepticons']} ) klz.provides :energy, platform: ['autobots', 'decepticons'] end it 'adds mappings for all platforms' do - expect(Chef::Resource.node_map).to receive(:set).with( + expect(Chef.resource_handler_map).to receive(:set).with( :tape_deck, Chef::Resource::Klz, {} ) klz.provides :tape_deck @@ -731,35 +832,51 @@ describe Chef::Resource do end - describe "lookups from the platform map" do - let(:klz1) { Class.new(Chef::Resource) } - let(:klz2) { Class.new(Chef::Resource) } + describe "resource_for_node" do + describe "lookups from the platform map" do + let(:klz1) { Class.new(Chef::Resource) } + + before(:each) do + Chef::Resource::Klz1 = klz1 + @node = Chef::Node.new + @node.name("bumblebee") + @node.automatic[:platform] = "autobots" + @node.automatic[:platform_version] = "6.1" + Object.const_set('Soundwave', klz1) + klz1.provides :soundwave + end - before(:each) do - Chef::Resource::Klz1 = klz1 - Chef::Resource::Klz2 = klz2 - @node = Chef::Node.new - @node.name("bumblebee") - @node.automatic[:platform] = "autobots" - @node.automatic[:platform_version] = "6.1" - Object.const_set('Soundwave', klz1) - klz2.provides :dinobot, :on_platforms => ['autobots'] - Object.const_set('Grimlock', klz2) - end + after(:each) do + Object.send(:remove_const, :Soundwave) + Chef::Resource.send(:remove_const, :Klz1) + end - after(:each) do - Object.send(:remove_const, :Soundwave) - Object.send(:remove_const, :Grimlock) - Chef::Resource.send(:remove_const, :Klz1) - Chef::Resource.send(:remove_const, :Klz2) + it "returns a resource by short_name if nothing else matches" do + expect(Chef::Resource.resource_for_node(:soundwave, @node)).to eql(klz1) + end end - describe "resource_for_node" do - it "returns a resource by short_name and node" do - expect(Chef::Resource.resource_for_node(:dinobot, @node)).to eql(Grimlock) + describe "lookups from the platform map" do + let(:klz2) { Class.new(Chef::Resource) } + + before(:each) do + Chef::Resource::Klz2 = klz2 + @node = Chef::Node.new + @node.name("bumblebee") + @node.automatic[:platform] = "autobots" + @node.automatic[:platform_version] = "6.1" + klz2.provides :dinobot, :platform => ['autobots'] + Object.const_set('Grimlock', klz2) + klz2.provides :grimlock end - it "returns a resource by short_name if nothing else matches" do - expect(Chef::Resource.resource_for_node(:soundwave, @node)).to eql(Soundwave) + + after(:each) do + Object.send(:remove_const, :Grimlock) + Chef::Resource.send(:remove_const, :Klz2) + end + + it "returns a resource by short_name and node" do + expect(Chef::Resource.resource_for_node(:dinobot, @node)).to eql(klz2) end end @@ -860,4 +977,90 @@ describe Chef::Resource do end end + + describe "#action" do + let(:resource_class) do + Class.new(described_class) do + allowed_actions(%i{one two}) + end + end + let(:resource) { resource_class.new('test', nil) } + subject { resource.action } + + context "with a no action" do + it { is_expected.to eq [:nothing] } + end + + context "with a default action" do + let(:resource_class) do + Class.new(described_class) do + default_action(:one) + end + end + it { is_expected.to eq [:one] } + end + + context "with a symbol action" do + before { resource.action(:one) } + it { is_expected.to eq [:one] } + end + + context "with a string action" do + before { resource.action('two') } + it { is_expected.to eq [:two] } + end + + context "with an array action" do + before { resource.action([:two, :one]) } + it { is_expected.to eq [:two, :one] } + end + + context "with an assignment" do + before { resource.action = :one } + it { is_expected.to eq [:one] } + end + + context "with an array assignment" do + before { resource.action = [:two, :one] } + it { is_expected.to eq [:two, :one] } + end + + context "with an invalid action" do + it { expect { resource.action(:three) }.to raise_error Chef::Exceptions::ValidationFailed } + end + + context "with an invalid assignment action" do + it { expect { resource.action = :three }.to raise_error Chef::Exceptions::ValidationFailed } + end + end + + describe ".default_action" do + let(:default_action) { } + let(:resource_class) do + actions = default_action + Class.new(described_class) do + default_action(actions) if actions + end + end + subject { resource_class.default_action } + + context "with no default actions" do + it { is_expected.to eq [:nothing] } + end + + context "with a symbol default action" do + let(:default_action) { :one } + it { is_expected.to eq [:one] } + end + + context "with a string default action" do + let(:default_action) { 'one' } + it { is_expected.to eq [:one] } + end + + context "with an array default action" do + let(:default_action) { [:two, :one] } + it { is_expected.to eq [:two, :one] } + end + end end diff --git a/spec/unit/rest_spec.rb b/spec/unit/rest_spec.rb index 1aa7ac84ee..3b04981610 100644 --- a/spec/unit/rest_spec.rb +++ b/spec/unit/rest_spec.rb @@ -69,8 +69,8 @@ describe Chef::REST do rest end - let(:standard_read_headers) {{"Accept"=>"application/json", "Accept"=>"application/json", "Accept-Encoding"=>"gzip;q=1.0,deflate;q=0.6,identity;q=0.3", "X-REMOTE-REQUEST-ID"=>request_id}} - let(:standard_write_headers) {{"Accept"=>"application/json", "Content-Type"=>"application/json", "Accept"=>"application/json", "Accept-Encoding"=>"gzip;q=1.0,deflate;q=0.6,identity;q=0.3", "X-REMOTE-REQUEST-ID"=>request_id}} + let(:standard_read_headers) {{"Accept"=>"application/json", "Accept"=>"application/json", "Accept-Encoding"=>"gzip;q=1.0,deflate;q=0.6,identity;q=0.3", "X-REMOTE-REQUEST-ID"=>request_id, 'X-Ops-Server-API-Version' => Chef::HTTP::Authenticator::DEFAULT_SERVER_API_VERSION}} + let(:standard_write_headers) {{"Accept"=>"application/json", "Content-Type"=>"application/json", "Accept"=>"application/json", "Accept-Encoding"=>"gzip;q=1.0,deflate;q=0.6,identity;q=0.3", "X-REMOTE-REQUEST-ID"=>request_id, 'X-Ops-Server-API-Version' => Chef::HTTP::Authenticator::DEFAULT_SERVER_API_VERSION}} before(:each) do Chef::Log.init(log_stringio) @@ -92,6 +92,15 @@ describe Chef::REST do Chef::REST.new(base_url, nil, nil, options) end + context 'when created with a chef zero URL' do + + let(:url) { "chefzero://localhost:1" } + + it "does not load the signing key" do + expect { Chef::REST.new(url) }.to_not raise_error + end + end + describe "calling an HTTP verb on a path or absolute URL" do it "adds a relative URL to the base url it was initialized with" do expect(rest.create_url("foo/bar/baz")).to eq(URI.parse(base_url + "/foo/bar/baz")) @@ -268,19 +277,6 @@ describe Chef::REST do rest end - let(:base_headers) do - { - 'Accept' => 'application/json', - 'X-Chef-Version' => Chef::VERSION, - 'Accept-Encoding' => Chef::REST::RESTRequest::ENCODING_GZIP_DEFLATE, - 'X-REMOTE-REQUEST-ID' => request_id - } - end - - let (:req_with_body_headers) do - base_headers.merge("Content-Type" => "application/json", "Content-Length" => '13') - end - before(:each) do Chef::Config[:ssl_client_cert] = nil Chef::Config[:ssl_client_key] = nil @@ -295,7 +291,8 @@ describe Chef::REST do 'X-Chef-Version' => Chef::VERSION, 'Accept-Encoding' => Chef::REST::RESTRequest::ENCODING_GZIP_DEFLATE, 'Host' => host_header, - 'X-REMOTE-REQUEST-ID' => request_id + 'X-REMOTE-REQUEST-ID' => request_id, + 'X-Ops-Server-API-Version' => Chef::HTTP::Authenticator::DEFAULT_SERVER_API_VERSION } end @@ -539,7 +536,7 @@ describe Chef::REST do end end end - end + end # as JSON API requests context "when streaming downloads to a tempfile" do let!(:tempfile) { Tempfile.open("chef-rspec-rest_spec-line-@{__LINE__}--") } @@ -577,7 +574,8 @@ describe Chef::REST do 'X-Chef-Version' => Chef::VERSION, 'Accept-Encoding' => Chef::REST::RESTRequest::ENCODING_GZIP_DEFLATE, 'Host' => host_header, - 'X-REMOTE-REQUEST-ID'=> request_id + 'X-REMOTE-REQUEST-ID'=> request_id, + 'X-Ops-Server-API-Version' => Chef::HTTP::Authenticator::DEFAULT_SERVER_API_VERSION } expect(Net::HTTP::Get).to receive(:new).with("/?foo=bar", expected_headers).and_return(request_mock) rest.streaming_request(url, {}) @@ -588,7 +586,8 @@ describe Chef::REST do 'X-Chef-Version' => Chef::VERSION, 'Accept-Encoding' => Chef::REST::RESTRequest::ENCODING_GZIP_DEFLATE, 'Host' => host_header, - 'X-REMOTE-REQUEST-ID'=> request_id + 'X-REMOTE-REQUEST-ID'=> request_id, + 'X-Ops-Server-API-Version' => Chef::HTTP::Authenticator::DEFAULT_SERVER_API_VERSION } expect(Net::HTTP::Get).to receive(:new).with("/?foo=bar", expected_headers).and_return(request_mock) rest.streaming_request(url, {}) @@ -686,7 +685,7 @@ describe Chef::REST do expect(block_called).to be_truthy end end - end + end # when making REST requests context "when following redirects" do let(:rest) do diff --git a/spec/unit/role_spec.rb b/spec/unit/role_spec.rb index 5421b5a7b3..ecc7945a08 100644 --- a/spec/unit/role_spec.rb +++ b/spec/unit/role_spec.rb @@ -21,7 +21,7 @@ require 'chef/role' describe Chef::Role do before(:each) do - allow(Chef::Platform).to receive(:windows?) { false } + allow(ChefConfig).to receive(:windows?) { false } @role = Chef::Role.new @role.name("ops_master") end @@ -217,7 +217,7 @@ describe Chef::Role do end - include_examples "to_json equalivent to Chef::JSONCompat.to_json" do + include_examples "to_json equivalent to Chef::JSONCompat.to_json" do let(:jsonable) { @role } end end diff --git a/spec/unit/run_context/child_run_context_spec.rb b/spec/unit/run_context/child_run_context_spec.rb new file mode 100644 index 0000000000..63586e459c --- /dev/null +++ b/spec/unit/run_context/child_run_context_spec.rb @@ -0,0 +1,133 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Tim Hinderliter (<tim@opscode.com>) +# Author:: Christopher Walters (<cw@opscode.com>) +# Copyright:: Copyright (c) 2008, 2010 Opscode, 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 'support/lib/library_load_order' + +describe Chef::RunContext::ChildRunContext do + context "with a run context with stuff in it" do + let(:chef_repo_path) { File.expand_path(File.join(CHEF_SPEC_DATA, "run_context", "cookbooks")) } + let(:cookbook_collection) { + cl = Chef::CookbookLoader.new(chef_repo_path) + cl.load_cookbooks + Chef::CookbookCollection.new(cl) + } + let(:node) { + node = Chef::Node.new + node.run_list << "test" << "test::one" << "test::two" + node + } + let(:events) { Chef::EventDispatch::Dispatcher.new } + let(:run_context) { Chef::RunContext.new(node, cookbook_collection, events) } + + context "and a child run context" do + let(:child) { run_context.create_child } + + it "parent_run_context is set to the parent" do + expect(child.parent_run_context).to eq run_context + end + + it "audits is not the same as the parent" do + expect(child.audits.object_id).not_to eq run_context.audits.object_id + child.audits['hi'] = 'lo' + expect(child.audits['hi']).to eq('lo') + expect(run_context.audits['hi']).not_to eq('lo') + end + + it "resource_collection is not the same as the parent" do + expect(child.resource_collection.object_id).not_to eq run_context.resource_collection.object_id + f = Chef::Resource::File.new('hi', child) + child.resource_collection.insert(f) + expect(child.resource_collection).to include f + expect(run_context.resource_collection).not_to include f + end + + it "immediate_notification_collection is not the same as the parent" do + expect(child.immediate_notification_collection.object_id).not_to eq run_context.immediate_notification_collection.object_id + src = Chef::Resource::File.new('hi', child) + dest = Chef::Resource::File.new('argh', child) + notification = Chef::Resource::Notification.new(dest, :create, src) + child.notifies_immediately(notification) + expect(child.immediate_notification_collection['file[hi]']).to eq([notification]) + expect(run_context.immediate_notification_collection['file[hi]']).not_to eq([notification]) + end + + it "immediate_notifications is not the same as the parent" do + src = Chef::Resource::File.new('hi', child) + dest = Chef::Resource::File.new('argh', child) + notification = Chef::Resource::Notification.new(dest, :create, src) + child.notifies_immediately(notification) + expect(child.immediate_notifications(src)).to eq([notification]) + expect(run_context.immediate_notifications(src)).not_to eq([notification]) + end + + it "delayed_notification_collection is not the same as the parent" do + expect(child.delayed_notification_collection.object_id).not_to eq run_context.delayed_notification_collection.object_id + src = Chef::Resource::File.new('hi', child) + dest = Chef::Resource::File.new('argh', child) + notification = Chef::Resource::Notification.new(dest, :create, src) + child.notifies_delayed(notification) + expect(child.delayed_notification_collection['file[hi]']).to eq([notification]) + expect(run_context.delayed_notification_collection['file[hi]']).not_to eq([notification]) + end + + it "delayed_notifications is not the same as the parent" do + src = Chef::Resource::File.new('hi', child) + dest = Chef::Resource::File.new('argh', child) + notification = Chef::Resource::Notification.new(dest, :create, src) + child.notifies_delayed(notification) + expect(child.delayed_notifications(src)).to eq([notification]) + expect(run_context.delayed_notifications(src)).not_to eq([notification]) + end + + it "create_child creates a child-of-child" do + c = child.create_child + expect(c.parent_run_context).to eq child + end + + context "after load('include::default')" do + before do + run_list = Chef::RunList.new('include::default').expand('_default') + # TODO not sure why we had to do this to get everything to work ... + node.automatic_attrs[:recipes] = [] + child.load(run_list) + end + + it "load_recipe loads into the child" do + expect(child.resource_collection).to be_empty + child.load_recipe("include::includee") + expect(child.resource_collection).not_to be_empty + end + + it "include_recipe loads into the child" do + expect(child.resource_collection).to be_empty + child.include_recipe("include::includee") + expect(child.resource_collection).not_to be_empty + end + + it "load_recipe_file loads into the child" do + expect(child.resource_collection).to be_empty + child.load_recipe_file(File.expand_path("include/recipes/includee.rb", chef_repo_path)) + expect(child.resource_collection).not_to be_empty + end + end + end + end +end diff --git a/spec/unit/run_context_spec.rb b/spec/unit/run_context_spec.rb index d656111a7d..99801575ef 100644 --- a/spec/unit/run_context_spec.rb +++ b/spec/unit/run_context_spec.rb @@ -53,6 +53,44 @@ describe Chef::RunContext do expect(run_context.node).to eq(node) end + it "loads up node[:cookbooks]" do + expect(run_context.node[:cookbooks]).to eql( + { + "circular-dep1" => { + "version" => "0.0.0", + }, + "circular-dep2" => { + "version" => "0.0.0", + }, + "dependency1" => { + "version" => "0.0.0", + }, + "dependency2" => { + "version" => "0.0.0", + }, + "include" => { + "version" => "0.0.0", + }, + "no-default-attr" => { + "version" => "0.0.0", + }, + "test" => { + "version" => "0.0.0", + }, + "test-with-circular-deps" => { + "version" => "0.0.0", + }, + "test-with-deps" => { + "version" => "0.0.0", + }, + } + ) + end + + it "has a nil parent_run_context" do + expect(run_context.parent_run_context).to be_nil + end + describe "loading cookbooks for a run list" do before do @@ -159,4 +197,45 @@ describe Chef::RunContext do expect(run_context.reboot_requested?).to be_falsey end end + + describe "notifications" do + let(:notification) { Chef::Resource::Notification.new(nil, nil, notifying_resource) } + + shared_context "notifying resource is a Chef::Resource" do + let(:notifying_resource) { Chef::Resource.new("gerbil") } + + it "should be keyed off the resource name" do + run_context.send(setter, notification) + expect(run_context.send(getter, notifying_resource)).to eq([notification]) + end + end + + shared_context "notifying resource is a subclass of Chef::Resource" do + let(:declared_type) { :alpaca } + let(:notifying_resource) { + r = Class.new(Chef::Resource).new("guinea pig") + r.declared_type = declared_type + r + } + + it "should be keyed off the resource declared key" do + run_context.send(setter, notification) + expect(run_context.send(getter, notifying_resource)).to eq([notification]) + end + end + + describe "of the immediate kind" do + let(:setter) { :notifies_immediately } + let(:getter) { :immediate_notifications } + include_context "notifying resource is a Chef::Resource" + include_context "notifying resource is a subclass of Chef::Resource" + end + + describe "of the delayed kind" do + let(:setter) { :notifies_delayed } + let(:getter) { :delayed_notifications } + include_context "notifying resource is a Chef::Resource" + include_context "notifying resource is a subclass of Chef::Resource" + end + end end diff --git a/spec/unit/run_list/versioned_recipe_list_spec.rb b/spec/unit/run_list/versioned_recipe_list_spec.rb index 209ac37fc1..9c3ecaa0dd 100644 --- a/spec/unit/run_list/versioned_recipe_list_spec.rb +++ b/spec/unit/run_list/versioned_recipe_list_spec.rb @@ -26,98 +26,165 @@ describe Chef::RunList::VersionedRecipeList do end end + let(:list) { described_class.new } + + let(:versioned_recipes) { [] } + + let(:recipes) { [] } + + before do + recipes.each { |r| list << r } + versioned_recipes.each {|r| list.add_recipe r[:name], r[:version]} + end + describe "add_recipe" do - before(:each) do - @list = Chef::RunList::VersionedRecipeList.new - @list << "apt" - @list << "god" - @list << "apache2" - end + + let(:recipes) { %w[ apt god apache2 ] } it "should append the recipe to the end of the list" do - @list.add_recipe "rails" - expect(@list).to eq(["apt", "god", "apache2", "rails"]) + list.add_recipe "rails" + expect(list).to eq(["apt", "god", "apache2", "rails"]) end it "should not duplicate entries" do - @list.add_recipe "apt" - expect(@list).to eq(["apt", "god", "apache2"]) + list.add_recipe "apt" + expect(list).to eq(["apt", "god", "apache2"]) end it "should allow you to specify a version" do - @list.add_recipe "rails", "1.0.0" - expect(@list).to eq(["apt", "god", "apache2", "rails"]) - expect(@list.with_versions).to include({:name => "rails", :version => "1.0.0"}) + list.add_recipe "rails", "1.0.0" + expect(list).to eq(["apt", "god", "apache2", "rails"]) + expect(list.with_versions).to include({:name => "rails", :version => "1.0.0"}) end it "should allow you to specify a version for a recipe that already exists" do - @list.add_recipe "apt", "1.2.3" - expect(@list).to eq(["apt", "god", "apache2"]) - expect(@list.with_versions).to include({:name => "apt", :version => "1.2.3"}) + list.add_recipe "apt", "1.2.3" + expect(list).to eq(["apt", "god", "apache2"]) + expect(list.with_versions).to include({:name => "apt", :version => "1.2.3"}) end it "should allow you to specify the same version of a recipe twice" do - @list.add_recipe "rails", "1.0.0" - @list.add_recipe "rails", "1.0.0" - expect(@list.with_versions).to include({:name => "rails", :version => "1.0.0"}) + list.add_recipe "rails", "1.0.0" + list.add_recipe "rails", "1.0.0" + expect(list.with_versions).to include({:name => "rails", :version => "1.0.0"}) end it "should allow you to spcify no version, even when a version already exists" do - @list.add_recipe "rails", "1.0.0" - @list.add_recipe "rails" - expect(@list.with_versions).to include({:name => "rails", :version => "1.0.0"}) + list.add_recipe "rails", "1.0.0" + list.add_recipe "rails" + expect(list.with_versions).to include({:name => "rails", :version => "1.0.0"}) end it "should not allow multiple versions of the same recipe" do - @list.add_recipe "rails", "1.0.0" - expect {@list.add_recipe "rails", "0.1.0"}.to raise_error Chef::Exceptions::CookbookVersionConflict + list.add_recipe "rails", "1.0.0" + expect {list.add_recipe "rails", "0.1.0"}.to raise_error Chef::Exceptions::CookbookVersionConflict end end describe "with_versions" do - before(:each) do - @recipes = [ + + let(:versioned_recipes) do + [ {:name => "apt", :version => "1.0.0"}, {:name => "god", :version => nil}, {:name => "apache2", :version => "0.0.1"} ] - @list = Chef::RunList::VersionedRecipeList.new - @recipes.each {|i| @list.add_recipe i[:name], i[:version]} end - it "should return an array of hashes with :name and :version" do - expect(@list.with_versions).to eq(@recipes) + expect(list.with_versions).to eq(versioned_recipes) end it "should retain the same order as the version-less list" do - with_versions = @list.with_versions - @list.each_with_index do |item, index| + with_versions = list.with_versions + list.each_with_index do |item, index| expect(with_versions[index][:name]).to eq(item) end end end describe "with_version_constraints" do - before(:each) do - @recipes = [ - {:name => "apt", :version => "~> 1.2.0"}, - {:name => "god", :version => nil}, - {:name => "apache2", :version => "0.0.1"} - ] - @list = Chef::RunList::VersionedRecipeList.new - @recipes.each {|i| @list.add_recipe i[:name], i[:version]} - @constraints = @recipes.map do |x| - { :name => x[:name], - :version_constraint => Chef::VersionConstraint.new(x[:version]) - } - end + + let(:versioned_recipes) do + [ + {:name => "apt", :version => "~> 1.2.0"}, + {:name => "god", :version => nil}, + {:name => "apache2", :version => "0.0.1"} + ] end + it "should return an array of hashes with :name and :version_constraint" do - @list.with_version_constraints.each do |x| - expect(x).to have_key :name - expect(x[:version_constraint]).not_to be nil + list.with_version_constraints.each_with_index do |recipe_spec, i| + + expected_recipe = versioned_recipes[i] + + expect(recipe_spec[:name]).to eq(expected_recipe[:name]) + expect(recipe_spec[:version_constraint]).to eq(Chef::VersionConstraint.new(expected_recipe[:version])) end end end + + describe "with_fully_qualified_names_and_version_constraints" do + + let(:fq_names) { list.with_fully_qualified_names_and_version_constraints } + + context "with bare cookbook names" do + + let(:recipes) { %w[ apache2 ] } + + it "gives $cookbook_name::default" do + expect(fq_names).to eq( %w[ apache2::default ] ) + end + + end + + context "with qualified recipe names but no versions" do + + let(:recipes) { %w[ mysql::server ] } + + it "returns the qualified recipe names" do + expect(fq_names).to eq( %w[ mysql::server ] ) + end + + end + + context "with unqualified names that have version constraints" do + + let(:versioned_recipes) do + [ + {:name => "apt", :version => "~> 1.2.0"}, + ] + end + + it "gives qualified names with their versions" do + expect(fq_names).to eq([ "apt::default@~> 1.2.0" ]) + end + + it "does not mutate the recipe name" do + expect(fq_names).to eq([ "apt::default@~> 1.2.0" ]) + expect(list).to eq( [ "apt" ] ) + end + + end + + context "with fully qualified names that have version constraints" do + + let(:versioned_recipes) do + [ + {:name => "apt::cacher", :version => "~> 1.2.0"}, + ] + end + + it "gives qualified names with their versions" do + expect(fq_names).to eq([ "apt::cacher@~> 1.2.0" ]) + end + + it "does not mutate the recipe name" do + expect(fq_names).to eq([ "apt::cacher@~> 1.2.0" ]) + expect(list).to eq( [ "apt::cacher" ] ) + end + + end + end + end diff --git a/spec/unit/run_list_spec.rb b/spec/unit/run_list_spec.rb index bf996de8c1..e150579431 100644 --- a/spec/unit/run_list_spec.rb +++ b/spec/unit/run_list_spec.rb @@ -307,7 +307,7 @@ describe Chef::RunList do expect(Chef::JSONCompat.to_json(@run_list)).to eq(Chef::JSONCompat.to_json(["recipe[nagios::client]", "role[production]", "recipe[apache2]"])) end - include_examples "to_json equalivent to Chef::JSONCompat.to_json" do + include_examples "to_json equivalent to Chef::JSONCompat.to_json" do let(:jsonable) { @run_list } end diff --git a/spec/unit/search/query_spec.rb b/spec/unit/search/query_spec.rb index 2fb197b183..59ac80f228 100644 --- a/spec/unit/search/query_spec.rb +++ b/spec/unit/search/query_spec.rb @@ -81,6 +81,9 @@ describe Chef::Search::Query do end describe "search" do + let(:query_string) { "search/node?q=platform:rhel&sort=X_CHEF_id_CHEF_X%20asc&start=0" } + let(:query_string_continue) { "search/node?q=platform:rhel&sort=X_CHEF_id_CHEF_X%20asc&start=4" } + let(:response) { { "rows" => [ { "name" => "my-name-is-node", @@ -140,6 +143,19 @@ describe Chef::Search::Query do "total" => 4 } } + let(:big_response) { + r = response.dup + r["total"] = 8 + r + } + + let(:big_response_end) { + r = response.dup + r["start"] = 4 + r["total"] = 8 + r + } + it "accepts a type as the first argument" do expect { query.search("node") }.not_to raise_error expect { query.search(:node) }.not_to raise_error @@ -195,6 +211,14 @@ describe Chef::Search::Query do query.search(:node, "*:*", sort: nil, start: 0, rows: 1) { |r| @call_me.do(r) } end + it "sends multiple API requests when the server indicates there is more data" do + expect(rest).to receive(:get_rest).with(query_string).and_return(big_response) + expect(rest).to receive(:get_rest).with(query_string_continue).and_return(big_response_end) + query.search(:node, "platform:rhel") do |r| + nil + end + end + context "when :filter_result is provided as a result" do include_context "filtered search" do let(:filter_key) { :filter_result } diff --git a/spec/unit/shell_spec.rb b/spec/unit/shell_spec.rb index 0e028f4359..379043a017 100644 --- a/spec/unit/shell_spec.rb +++ b/spec/unit/shell_spec.rb @@ -43,6 +43,8 @@ describe Shell do before do Shell.irb_conf = {} allow(Shell::ShellSession.instance).to receive(:reset!) + allow(ChefConfig).to receive(:windows?).and_return(false) + allow(Chef::Util::PathHelper).to receive(:home).and_return('/home/foo') end describe "reporting its status" do @@ -56,7 +58,7 @@ describe Shell do describe "configuring IRB" do it "configures irb history" do Shell.configure_irb - expect(Shell.irb_conf[:HISTORY_FILE]).to eq("~/.chef/chef_shell_history") + expect(Shell.irb_conf[:HISTORY_FILE]).to eq(Chef::Util::PathHelper.home('.chef', 'chef_shell_history')) expect(Shell.irb_conf[:SAVE_HISTORY]).to eq(1000) end @@ -69,7 +71,7 @@ describe Shell do Shell.irb_conf[:IRB_RC].call(conf) expect(conf.prompt_c).to eq("chef > ") expect(conf.return_format).to eq(" => %s \n") - expect(conf.prompt_i).to eq("chef > ") + expect(conf.prompt_i).to eq("chef (#{Chef::VERSION})> ") expect(conf.prompt_n).to eq("chef ?> ") expect(conf.prompt_s).to eq("chef%l> ") expect(conf.use_tracer).to eq(false) @@ -83,7 +85,7 @@ describe Shell do conf.main = Chef::Recipe.new(nil,nil,Chef::RunContext.new(Chef::Node.new, {}, events)) Shell.irb_conf[:IRB_RC].call(conf) expect(conf.prompt_c).to eq("chef:recipe > ") - expect(conf.prompt_i).to eq("chef:recipe > ") + expect(conf.prompt_i).to eq("chef:recipe (#{Chef::VERSION})> ") expect(conf.prompt_n).to eq("chef:recipe ?> ") expect(conf.prompt_s).to eq("chef:recipe%l> ") end @@ -95,7 +97,7 @@ describe Shell do conf.main = Chef::Node.new Shell.irb_conf[:IRB_RC].call(conf) expect(conf.prompt_c).to eq("chef:attributes > ") - expect(conf.prompt_i).to eq("chef:attributes > ") + expect(conf.prompt_i).to eq("chef:attributes (#{Chef::VERSION})> ") expect(conf.prompt_n).to eq("chef:attributes ?> ") expect(conf.prompt_s).to eq("chef:attributes%l> ") end diff --git a/spec/unit/user_spec.rb b/spec/unit/user_spec.rb index d451531b16..97cc32eb3e 100644 --- a/spec/unit/user_spec.rb +++ b/spec/unit/user_spec.rb @@ -16,6 +16,11 @@ # limitations under the License. # +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur in user_spec.rb. + require 'spec_helper' require 'chef/user' @@ -155,7 +160,7 @@ describe Chef::User do expect(@json).not_to include("password") end - include_examples "to_json equalivent to Chef::JSONCompat.to_json" do + include_examples "to_json equivalent to Chef::JSONCompat.to_json" do let(:jsonable) { @user } end end @@ -200,8 +205,8 @@ describe Chef::User do before (:each) do @user = Chef::User.new @user.name "foobar" - @http_client = double("Chef::REST mock") - allow(Chef::REST).to receive(:new).and_return(@http_client) + @http_client = double("Chef::ServerAPI mock") + allow(Chef::ServerAPI).to receive(:new).and_return(@http_client) end describe "list" do @@ -214,24 +219,24 @@ describe Chef::User do end it "lists all clients on an OSC server" do - allow(@http_client).to receive(:get_rest).with("users").and_return(@osc_response) + allow(@http_client).to receive(:get).with("users").and_return(@osc_response) expect(Chef::User.list).to eq(@osc_response) end it "inflate all clients on an OSC server" do - allow(@http_client).to receive(:get_rest).with("users").and_return(@osc_response) + allow(@http_client).to receive(:get).with("users").and_return(@osc_response) expect(Chef::User.list(true)).to eq(@osc_inflated_response) end it "lists all clients on an OHC/OPC server" do - allow(@http_client).to receive(:get_rest).with("users").and_return(@ohc_response) + allow(@http_client).to receive(:get).with("users").and_return(@ohc_response) # We expect that Chef::User.list will give a consistent response # so OHC API responses should be transformed to OSC-style output. expect(Chef::User.list).to eq(@osc_response) end it "inflate all clients on an OHC/OPC server" do - allow(@http_client).to receive(:get_rest).with("users").and_return(@ohc_response) + allow(@http_client).to receive(:get).with("users").and_return(@ohc_response) expect(Chef::User.list(true)).to eq(@osc_inflated_response) end end @@ -239,14 +244,14 @@ describe Chef::User do describe "create" do it "creates a new user via the API" do @user.password "password" - expect(@http_client).to receive(:post_rest).with("users", {:name => "foobar", :admin => false, :password => "password"}).and_return({}) + expect(@http_client).to receive(:post).with("users", {:name => "foobar", :admin => false, :password => "password"}).and_return({}) @user.create end end describe "read" do it "loads a named user from the API" do - expect(@http_client).to receive(:get_rest).with("users/foobar").and_return({"name" => "foobar", "admin" => true, "public_key" => "pubkey"}) + expect(@http_client).to receive(:get).with("users/foobar").and_return({"name" => "foobar", "admin" => true, "public_key" => "pubkey"}) user = Chef::User.load("foobar") expect(user.name).to eq("foobar") expect(user.admin).to eq(true) @@ -256,14 +261,14 @@ describe Chef::User do describe "update" do it "updates an existing user on via the API" do - expect(@http_client).to receive(:put_rest).with("users/foobar", {:name => "foobar", :admin => false}).and_return({}) + expect(@http_client).to receive(:put).with("users/foobar", {:name => "foobar", :admin => false}).and_return({}) @user.update end end describe "destroy" do it "deletes the specified user via the API" do - expect(@http_client).to receive(:delete_rest).with("users/foobar") + expect(@http_client).to receive(:delete).with("users/foobar") @user.destroy end end diff --git a/spec/unit/user_v1_spec.rb b/spec/unit/user_v1_spec.rb new file mode 100644 index 0000000000..8fd370a010 --- /dev/null +++ b/spec/unit/user_v1_spec.rb @@ -0,0 +1,584 @@ +# +# Author:: Steven Danna (steve@opscode.com) +# Copyright:: Copyright (c) 2012 Opscode, 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/user_v1' +require 'tempfile' + +describe Chef::UserV1 do + before(:each) do + @user = Chef::UserV1.new + end + + shared_examples_for "string fields with no contraints" do + it "should let you set the public key" do + expect(@user.send(method, "some_string")).to eq("some_string") + end + + it "should return the current public key" do + @user.send(method, "some_string") + expect(@user.send(method)).to eq("some_string") + end + + it "should throw an ArgumentError if you feed it something lame" do + expect { @user.send(method, Hash.new) }.to raise_error(ArgumentError) + end + end + + shared_examples_for "boolean fields with no constraints" do + it "should let you set the field" do + expect(@user.send(method, true)).to eq(true) + end + + it "should return the current field value" do + @user.send(method, true) + expect(@user.send(method)).to eq(true) + end + + it "should return the false value when false" do + @user.send(method, false) + expect(@user.send(method)).to eq(false) + end + + it "should throw an ArgumentError if you feed it anything but true or false" do + expect { @user.send(method, Hash.new) }.to raise_error(ArgumentError) + end + end + + describe "initialize" do + it "should be a Chef::UserV1" do + expect(@user).to be_a_kind_of(Chef::UserV1) + end + end + + describe "username" do + it "should let you set the username to a string" do + expect(@user.username("ops_master")).to eq("ops_master") + end + + it "should return the current username" do + @user.username "ops_master" + expect(@user.username).to eq("ops_master") + end + + # It is not feasible to check all invalid characters. Here are a few + # that we probably care about. + it "should not accept invalid characters" do + # capital letters + expect { @user.username "Bar" }.to raise_error(ArgumentError) + # slashes + expect { @user.username "foo/bar" }.to raise_error(ArgumentError) + # ? + expect { @user.username "foo?" }.to raise_error(ArgumentError) + # & + expect { @user.username "foo&" }.to raise_error(ArgumentError) + end + + + it "should not accept spaces" do + expect { @user.username "ops master" }.to raise_error(ArgumentError) + end + + it "should throw an ArgumentError if you feed it anything but a string" do + expect { @user.username Hash.new }.to raise_error(ArgumentError) + end + end + + describe "boolean fields" do + describe "create_key" do + it_should_behave_like "boolean fields with no constraints" do + let(:method) { :create_key } + end + end + end + + describe "string fields" do + describe "public_key" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :public_key } + end + end + + describe "private_key" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :private_key } + end + end + + describe "display_name" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :display_name } + end + end + + describe "first_name" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :first_name } + end + end + + describe "middle_name" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :middle_name } + end + end + + describe "last_name" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :last_name } + end + end + + describe "email" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :email } + end + end + + describe "password" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :password } + end + end + end + + describe "when serializing to JSON" do + before(:each) do + @user.username("black") + @json = @user.to_json + end + + it "serializes as a JSON object" do + expect(@json).to match(/^\{.+\}$/) + end + + it "includes the username value" do + expect(@json).to include(%q{"username":"black"}) + end + + it "includes the display name when present" do + @user.display_name("get_displayed") + expect(@user.to_json).to include(%{"display_name":"get_displayed"}) + end + + it "does not include the display name if not present" do + expect(@json).not_to include("display_name") + end + + it "includes the first name when present" do + @user.first_name("char") + expect(@user.to_json).to include(%{"first_name":"char"}) + end + + it "does not include the first name if not present" do + expect(@json).not_to include("first_name") + end + + it "includes the middle name when present" do + @user.middle_name("man") + expect(@user.to_json).to include(%{"middle_name":"man"}) + end + + it "does not include the middle name if not present" do + expect(@json).not_to include("middle_name") + end + + it "includes the last name when present" do + @user.last_name("der") + expect(@user.to_json).to include(%{"last_name":"der"}) + end + + it "does not include the last name if not present" do + expect(@json).not_to include("last_name") + end + + it "includes the email when present" do + @user.email("charmander@pokemon.poke") + expect(@user.to_json).to include(%{"email":"charmander@pokemon.poke"}) + end + + it "does not include the email if not present" do + expect(@json).not_to include("email") + end + + it "includes the public key when present" do + @user.public_key("crowes") + expect(@user.to_json).to include(%{"public_key":"crowes"}) + end + + it "does not include the public key if not present" do + expect(@json).not_to include("public_key") + end + + it "includes the private key when present" do + @user.private_key("monkeypants") + expect(@user.to_json).to include(%q{"private_key":"monkeypants"}) + end + + it "does not include the private key if not present" do + expect(@json).not_to include("private_key") + end + + it "includes the password if present" do + @user.password "password" + expect(@user.to_json).to include(%q{"password":"password"}) + end + + it "does not include the password if not present" do + expect(@json).not_to include("password") + end + + include_examples "to_json equivalent to Chef::JSONCompat.to_json" do + let(:jsonable) { @user } + end + end + + describe "when deserializing from JSON" do + before(:each) do + user = { + "username" => "mr_spinks", + "display_name" => "displayed", + "first_name" => "char", + "middle_name" => "man", + "last_name" => "der", + "email" => "charmander@pokemon.poke", + "password" => "password", + "public_key" => "turtles", + "private_key" => "pandas", + "create_key" => false + } + @user = Chef::UserV1.from_json(Chef::JSONCompat.to_json(user)) + end + + it "should deserialize to a Chef::UserV1 object" do + expect(@user).to be_a_kind_of(Chef::UserV1) + end + + it "preserves the username" do + expect(@user.username).to eq("mr_spinks") + end + + it "preserves the display name if present" do + expect(@user.display_name).to eq("displayed") + end + + it "preserves the first name if present" do + expect(@user.first_name).to eq("char") + end + + it "preserves the middle name if present" do + expect(@user.middle_name).to eq("man") + end + + it "preserves the last name if present" do + expect(@user.last_name).to eq("der") + end + + it "preserves the email if present" do + expect(@user.email).to eq("charmander@pokemon.poke") + end + + it "includes the password if present" do + expect(@user.password).to eq("password") + end + + it "preserves the public key if present" do + expect(@user.public_key).to eq("turtles") + end + + it "includes the private key if present" do + expect(@user.private_key).to eq("pandas") + end + + it "includes the create key status if not nil" do + expect(@user.create_key).to be_falsey + end + end + + describe "Versioned API Interactions" do + let(:response_406) { OpenStruct.new(:code => '406') } + let(:exception_406) { Net::HTTPServerException.new("406 Not Acceptable", response_406) } + + before (:each) do + @user = Chef::UserV1.new + allow(@user).to receive(:chef_root_rest_v0).and_return(double('chef rest root v0 object')) + allow(@user).to receive(:chef_root_rest_v1).and_return(double('chef rest root v1 object')) + end + + describe "update" do + before do + # populate all fields that are valid between V0 and V1 + @user.username "some_username" + @user.display_name "some_display_name" + @user.first_name "some_first_name" + @user.middle_name "some_middle_name" + @user.last_name "some_last_name" + @user.email "some_email" + @user.password "some_password" + end + + let(:payload) { + { + :username => "some_username", + :display_name => "some_display_name", + :first_name => "some_first_name", + :middle_name => "some_middle_name", + :last_name => "some_last_name", + :email => "some_email", + :password => "some_password" + } + } + + context "when server API V1 is valid on the Chef Server receiving the request" do + context "when the user submits valid data" do + it "properly updates the user" do + expect(@user.chef_root_rest_v1).to receive(:put).with("users/some_username", payload).and_return({}) + @user.update + end + end + end + + context "when server API V1 is not valid on the Chef Server receiving the request" do + let(:payload) { + { + :username => "some_username", + :display_name => "some_display_name", + :first_name => "some_first_name", + :middle_name => "some_middle_name", + :last_name => "some_last_name", + :email => "some_email", + :password => "some_password", + :public_key => "some_public_key" + } + } + + before do + @user.public_key "some_public_key" + allow(@user.chef_root_rest_v1).to receive(:put) + end + + context "when the server returns a 400" do + let(:response_400) { OpenStruct.new(:code => '400') } + let(:exception_400) { Net::HTTPServerException.new("400 Bad Request", response_400) } + + context "when the 400 was due to public / private key fields no longer being supported" do + let(:response_body_400) { '{"error":["Since Server API v1, all keys must be updated via the keys endpoint. "]}' } + + before do + allow(response_400).to receive(:body).and_return(response_body_400) + allow(@user.chef_root_rest_v1).to receive(:put).and_raise(exception_400) + end + + it "proceeds with the V0 PUT since it can handle public / private key fields" do + expect(@user.chef_root_rest_v0).to receive(:put).with("users/some_username", payload).and_return({}) + @user.update + end + + it "does not call server_client_api_version_intersection, since we know to proceed with V0 in this case" do + expect(@user).to_not receive(:server_client_api_version_intersection) + allow(@user.chef_root_rest_v0).to receive(:put).and_return({}) + @user.update + end + end # when the 400 was due to public / private key fields + + context "when the 400 was NOT due to public / private key fields no longer being supported" do + let(:response_body_400) { '{"error":["Some other error. "]}' } + + before do + allow(response_400).to receive(:body).and_return(response_body_400) + allow(@user.chef_root_rest_v1).to receive(:put).and_raise(exception_400) + end + + it "will not proceed with the V0 PUT since the original bad request was not key related" do + expect(@user.chef_root_rest_v0).to_not receive(:put).with("users/some_username", payload) + expect { @user.update }.to raise_error(exception_400) + end + + it "raises the original error" do + expect { @user.update }.to raise_error(exception_400) + end + + end + end # when the server returns a 400 + + context "when the server returns a 406" do + # from spec/support/shared/unit/api_versioning.rb + it_should_behave_like "version handling" do + let(:object) { @user } + let(:method) { :update } + let(:http_verb) { :put } + let(:rest_v1) { @user.chef_root_rest_v1 } + end + + context "when the server supports API V0" do + before do + allow(@user).to receive(:server_client_api_version_intersection).and_return([0]) + allow(@user.chef_root_rest_v1).to receive(:put).and_raise(exception_406) + end + + it "properly updates the user" do + expect(@user.chef_root_rest_v0).to receive(:put).with("users/some_username", payload).and_return({}) + @user.update + end + end # when the server supports API V0 + end # when the server returns a 406 + + end # when server API V1 is not valid on the Chef Server receiving the request + end # update + + describe "create" do + let(:payload) { + { + :username => "some_username", + :display_name => "some_display_name", + :first_name => "some_first_name", + :last_name => "some_last_name", + :email => "some_email", + :password => "some_password" + } + } + before do + @user.username "some_username" + @user.display_name "some_display_name" + @user.first_name "some_first_name" + @user.last_name "some_last_name" + @user.email "some_email" + @user.password "some_password" + end + + # from spec/support/shared/unit/user_and_client_shared.rb + it_should_behave_like "user or client create" do + let(:object) { @user } + let(:error) { Chef::Exceptions::InvalidUserAttribute } + let(:rest_v0) { @user.chef_root_rest_v0 } + let(:rest_v1) { @user.chef_root_rest_v1 } + let(:url) { "users" } + end + + context "when handling API V1" do + it "creates a new user via the API with a middle_name when it exists" do + @user.middle_name "some_middle_name" + expect(@user.chef_root_rest_v1).to receive(:post).with("users", payload.merge({:middle_name => "some_middle_name"})).and_return({}) + @user.create + end + end # when server API V1 is valid on the Chef Server receiving the request + + context "when API V1 is not supported by the server" do + # from spec/support/shared/unit/api_versioning.rb + it_should_behave_like "version handling" do + let(:object) { @user } + let(:method) { :create } + let(:http_verb) { :post } + let(:rest_v1) { @user.chef_root_rest_v1 } + end + end + + context "when handling API V0" do + before do + allow(@user).to receive(:server_client_api_version_intersection).and_return([0]) + allow(@user.chef_root_rest_v1).to receive(:post).and_raise(exception_406) + end + + it "creates a new user via the API with a middle_name when it exists" do + @user.middle_name "some_middle_name" + expect(@user.chef_root_rest_v0).to receive(:post).with("users", payload.merge({:middle_name => "some_middle_name"})).and_return({}) + @user.create + end + end # when server API V1 is not valid on the Chef Server receiving the request + + end # create + + # DEPRECATION + # This can be removed after API V0 support is gone + describe "reregister" do + let(:payload) { + { + "username" => "some_username", + } + } + + before do + @user.username "some_username" + end + + context "when server API V0 is valid on the Chef Server receiving the request" do + it "creates a new object via the API" do + expect(@user.chef_root_rest_v0).to receive(:put).with("users/#{@user.username}", payload.merge({"private_key" => true})).and_return({}) + @user.reregister + end + end # when server API V0 is valid on the Chef Server receiving the request + + context "when server API V0 is not supported by the Chef Server" do + # from spec/support/shared/unit/api_versioning.rb + it_should_behave_like "user and client reregister" do + let(:object) { @user } + let(:rest_v0) { @user.chef_root_rest_v0 } + end + end # when server API V0 is not supported by the Chef Server + end # reregister + + end # Versioned API Interactions + + describe "API Interactions" do + before (:each) do + @user = Chef::UserV1.new + @user.username "foobar" + @http_client = double("Chef::REST mock") + allow(Chef::REST).to receive(:new).and_return(@http_client) + end + + describe "list" do + before(:each) do + Chef::Config[:chef_server_url] = "http://www.example.com" + @osc_response = { "admin" => "http://www.example.com/users/admin"} + @ohc_response = [ { "user" => { "username" => "admin" }} ] + allow(Chef::UserV1).to receive(:load).with("admin").and_return(@user) + @osc_inflated_response = { "admin" => @user } + end + + it "lists all clients on an OHC/OPC server" do + allow(@http_client).to receive(:get).with("users").and_return(@ohc_response) + # We expect that Chef::UserV1.list will give a consistent response + # so OHC API responses should be transformed to OSC-style output. + expect(Chef::UserV1.list).to eq(@osc_response) + end + + it "inflate all clients on an OHC/OPC server" do + allow(@http_client).to receive(:get).with("users").and_return(@ohc_response) + expect(Chef::UserV1.list(true)).to eq(@osc_inflated_response) + end + end + + describe "read" do + it "loads a named user from the API" do + expect(@http_client).to receive(:get).with("users/foobar").and_return({"username" => "foobar", "admin" => true, "public_key" => "pubkey"}) + user = Chef::UserV1.load("foobar") + expect(user.username).to eq("foobar") + expect(user.public_key).to eq("pubkey") + end + end + + describe "destroy" do + it "deletes the specified user via the API" do + expect(@http_client).to receive(:delete).with("users/foobar") + @user.destroy + end + end + end +end diff --git a/spec/unit/util/dsc/resource_store.rb b/spec/unit/util/dsc/resource_store.rb new file mode 100644 index 0000000000..a89e73fcaa --- /dev/null +++ b/spec/unit/util/dsc/resource_store.rb @@ -0,0 +1,76 @@ +# +# Author:: Jay Mundrawala <jdm@chef.io> +# 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 'chef' +require 'chef/util/dsc/resource_store' + +describe Chef::Util::DSC::ResourceStore do + let(:resource_store) { Chef::Util::DSC::ResourceStore.new } + let(:resource_a) { { + 'ResourceType' => 'AFoo', + 'Name' => 'Foo', + 'Module' => {'Name' => 'ModuleA'} + } + } + + let(:resource_b) { { + 'ResourceType' => 'BFoo', + 'Name' => 'Foo', + 'Module' => {'Name' => 'ModuleB'} + } + } + + context 'when resources are not cached' do + context 'when calling #resources' do + it 'returns an empty array' do + expect(resource_store.resources).to eql([]) + end + end + + context 'when calling #find' do + it 'returns an empty list if it cannot find any matching resources' do + expect(resource_store).to receive(:query_resource).and_return([]) + expect(resource_store.find('foo')).to eql([]) + end + + it 'returns the resource if it is found (comparisons are case insensitive)' do + expect(resource_store).to receive(:query_resource).and_return([resource_a]) + expect(resource_store.find('foo')).to eql([resource_a]) + end + + it 'returns multiple resoures if they are found' do + expect(resource_store).to receive(:query_resource).and_return([resource_a, resource_b]) + expect(resource_store.find('foo')).to include(resource_a, resource_b) + end + + it 'deduplicates resources by ResourceName' do + expect(resource_store).to receive(:query_resource).and_return([resource_a, resource_a]) + resource_store.find('foo') + expect(resource_store.resources).to eq([resource_a]) + end + end + end + + context 'when resources are cached' do + it 'recalls resources from the cache if present' do + expect(resource_store).not_to receive(:query_resource) + expect(resource_store).to receive(:resources).and_return([resource_a]) + resource_store.find('foo') + end + end +end diff --git a/spec/unit/util/path_helper_spec.rb b/spec/unit/util/path_helper_spec.rb deleted file mode 100644 index 5756c29b90..0000000000 --- a/spec/unit/util/path_helper_spec.rb +++ /dev/null @@ -1,233 +0,0 @@ -# -# 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/util/path_helper' -require 'spec_helper' - -describe Chef::Util::PathHelper do - PathHelper = Chef::Util::PathHelper - - [ false, true ].each do |is_windows| - context "on #{is_windows ? "windows" : "unix"}" do - before(:each) do - allow(Chef::Platform).to receive(:windows?).and_return(is_windows) - end - - describe "join" do - it "joins components when some end with separators" do - expected = PathHelper.cleanpath("/foo/bar/baz") - expected = "C:#{expected}" if is_windows - expect(PathHelper.join(is_windows ? 'C:\\foo\\' : "/foo/", "bar", "baz")).to eq(expected) - end - - it "joins components when some end and start with separators" do - expected = PathHelper.cleanpath("/foo/bar/baz") - expected = "C:#{expected}" if is_windows - expect(PathHelper.join(is_windows ? 'C:\\foo\\' : "/foo/", "bar/", "/baz")).to eq(expected) - end - - it "joins components that don't end in separators" do - expected = PathHelper.cleanpath("/foo/bar/baz") - expected = "C:#{expected}" if is_windows - expect(PathHelper.join(is_windows ? 'C:\\foo' : "/foo", "bar", "baz")).to eq(expected) - end - - it "joins starting with '' resolve to absolute paths" do - expect(PathHelper.join('', 'a', 'b')).to eq("#{PathHelper.path_separator}a#{PathHelper.path_separator}b") - end - - it "joins ending with '' add a / to the end" do - expect(PathHelper.join('a', 'b', '')).to eq("a#{PathHelper.path_separator}b#{PathHelper.path_separator}") - end - - if is_windows - it "joins components on Windows when some end with unix separators" do - expect(PathHelper.join('C:\\foo/', "bar", "baz")).to eq('C:\\foo\\bar\\baz') - end - end - end - - if is_windows - it "path_separator is \\" do - expect(PathHelper.path_separator).to eq('\\') - end - else - it "path_separator is /" do - expect(PathHelper.path_separator).to eq('/') - end - end - - if is_windows - it "cleanpath changes slashes into backslashes and leaves backslashes alone" do - expect(PathHelper.cleanpath('/a/b\\c/d/')).to eq('\\a\\b\\c\\d') - end - it "cleanpath does not remove leading double backslash" do - expect(PathHelper.cleanpath('\\\\a/b\\c/d/')).to eq('\\\\a\\b\\c\\d') - end - else - it "cleanpath removes extra slashes alone" do - expect(PathHelper.cleanpath('/a///b/c/d/')).to eq('/a/b/c/d') - end - end - - describe "dirname" do - it "dirname('abc') is '.'" do - expect(PathHelper.dirname('abc')).to eq('.') - end - it "dirname('/') is '/'" do - expect(PathHelper.dirname(PathHelper.path_separator)).to eq(PathHelper.path_separator) - end - it "dirname('a/b/c') is 'a/b'" do - expect(PathHelper.dirname(PathHelper.join('a', 'b', 'c'))).to eq(PathHelper.join('a', 'b')) - end - it "dirname('a/b/c/') is 'a/b'" do - expect(PathHelper.dirname(PathHelper.join('a', 'b', 'c', ''))).to eq(PathHelper.join('a', 'b')) - end - it "dirname('/a/b/c') is '/a/b'" do - expect(PathHelper.dirname(PathHelper.join('', 'a', 'b', 'c'))).to eq(PathHelper.join('', 'a', 'b')) - end - end - end - end - - describe "validate_path" do - context "on windows" do - before(:each) do - # pass by default - allow(Chef::Platform).to receive(:windows?).and_return(true) - allow(PathHelper).to receive(:printable?).and_return(true) - allow(PathHelper).to receive(:windows_max_length_exceeded?).and_return(false) - end - - it "returns the path if the path passes the tests" do - expect(PathHelper.validate_path("C:\\ThisIsRigged")).to eql("C:\\ThisIsRigged") - end - - it "does not raise an error if everything looks great" do - expect { PathHelper.validate_path("C:\\cool path\\dude.exe") }.not_to raise_error - end - - it "raises an error if the path has invalid characters" do - allow(PathHelper).to receive(:printable?).and_return(false) - expect { PathHelper.validate_path("Newline!\n") }.to raise_error(Chef::Exceptions::ValidationFailed) - end - - it "Adds the \\\\?\\ prefix if the path exceeds MAX_LENGTH and does not have it" do - long_path = "C:\\" + "a" * 250 + "\\" + "b" * 250 - prefixed_long_path = "\\\\?\\" + long_path - allow(PathHelper).to receive(:windows_max_length_exceeded?).and_return(true) - expect(PathHelper.validate_path(long_path)).to eql(prefixed_long_path) - end - end - end - - describe "windows_max_length_exceeded?" do - it "returns true if the path is too long (259 + NUL) for the API" do - expect(PathHelper.windows_max_length_exceeded?("C:\\" + "a" * 250 + "\\" + "b" * 6)).to be_truthy - end - - it "returns false if the path is not too long (259 + NUL) for the standard API" do - expect(PathHelper.windows_max_length_exceeded?("C:\\" + "a" * 250 + "\\" + "b" * 5)).to be_falsey - end - - it "returns false if the path is over 259 characters but uses the \\\\?\\ prefix" do - expect(PathHelper.windows_max_length_exceeded?("\\\\?\\C:\\" + "a" * 250 + "\\" + "b" * 250)).to be_falsey - end - end - - describe "printable?" do - it "returns true if the string contains no non-printable characters" do - expect(PathHelper.printable?("C:\\Program Files (x86)\\Microsoft Office\\Files.lst")).to be_truthy - end - - it "returns true when given 'abc' in unicode" do - expect(PathHelper.printable?("\u0061\u0062\u0063")).to be_truthy - end - - it "returns true when given japanese unicode" do - expect(PathHelper.printable?("\uff86\uff87\uff88")).to be_truthy - end - - it "returns false if the string contains a non-printable character" do - expect(PathHelper.printable?("\my files\work\notes.txt")).to be_falsey - end - - # This isn't necessarily a requirement, but here to be explicit about functionality. - it "returns false if the string contains a newline or tab" do - expect(PathHelper.printable?("\tThere's no way,\n\t *no* way,\n\t that you came from my loins.\n")).to be_falsey - end - end - - describe "canonical_path" do - context "on windows", :windows_only do - it "returns an absolute path with backslashes instead of slashes" do - expect(PathHelper.canonical_path("\\\\?\\C:/windows/win.ini")).to eq("\\\\?\\c:\\windows\\win.ini") - end - - it "adds the \\\\?\\ prefix if it is missing" do - expect(PathHelper.canonical_path("C:/windows/win.ini")).to eq("\\\\?\\c:\\windows\\win.ini") - end - - it "returns a lowercase path" do - expect(PathHelper.canonical_path("\\\\?\\C:\\CASE\\INSENSITIVE")).to eq("\\\\?\\c:\\case\\insensitive") - end - end - - context "not on windows", :unix_only do - it "returns a canonical path" do - expect(PathHelper.canonical_path("/etc//apache.d/sites-enabled/../sites-available/default")).to eq("/etc/apache.d/sites-available/default") - end - end - end - - describe "paths_eql?" do - it "returns true if the paths are the same" do - allow(PathHelper).to receive(:canonical_path).with("bandit").and_return("c:/bandit/bandit") - allow(PathHelper).to receive(:canonical_path).with("../bandit/bandit").and_return("c:/bandit/bandit") - expect(PathHelper.paths_eql?("bandit", "../bandit/bandit")).to be_truthy - end - - it "returns false if the paths are different" do - allow(PathHelper).to receive(:canonical_path).with("bandit").and_return("c:/Bo/Bandit") - allow(PathHelper).to receive(:canonical_path).with("../bandit/bandit").and_return("c:/bandit/bandit") - expect(PathHelper.paths_eql?("bandit", "../bandit/bandit")).to be_falsey - end - end - - describe "escape_glob" do - it "escapes characters reserved by glob" do - path = "C:\\this\\*path\\[needs]\\escaping?" - escaped_path = "C:\\\\this\\\\\\*path\\\\\\[needs\\]\\\\escaping\\?" - expect(PathHelper.escape_glob(path)).to eq(escaped_path) - end - - context "when given more than one argument" do - it "joins, cleanpaths, and escapes characters reserved by glob" do - args = ["this/*path", "[needs]", "escaping?"] - escaped_path = if windows? - "this\\\\\\*path\\\\\\[needs\\]\\\\escaping\\?" - else - "this/\\*path/\\[needs\\]/escaping\\?" - end - expect(PathHelper).to receive(:join).with(*args).and_call_original - expect(PathHelper).to receive(:cleanpath).and_call_original - expect(PathHelper.escape_glob(*args)).to eq(escaped_path) - end - end - end -end diff --git a/spec/unit/util/powershell/ps_credential_spec.rb b/spec/unit/util/powershell/ps_credential_spec.rb new file mode 100644 index 0000000000..bac58b02e5 --- /dev/null +++ b/spec/unit/util/powershell/ps_credential_spec.rb @@ -0,0 +1,37 @@ +# +# Author:: Jay Mundrawala <jdm@chef.io> +# 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 'chef' +require 'chef/util/powershell/ps_credential' + +describe Chef::Util::Powershell::PSCredential do + let (:username) { 'foo' } + let (:password) { 'password' } + + context 'when username and password are provided' do + let(:ps_credential) { Chef::Util::Powershell::PSCredential.new(username, password)} + context 'when calling to_psobject' do + it 'should create the script to create a PSCredential when calling' do + allow(ps_credential).to receive(:encrypt).with(password).and_return('encrypted') + expect(ps_credential.to_psobject).to eq( + "New-Object System.Management.Automation.PSCredential("\ + "'#{username}',('encrypted' | ConvertTo-SecureString))") + end + end + end +end diff --git a/spec/unit/workstation_config_loader_spec.rb b/spec/unit/workstation_config_loader_spec.rb deleted file mode 100644 index a865103188..0000000000 --- a/spec/unit/workstation_config_loader_spec.rb +++ /dev/null @@ -1,283 +0,0 @@ -# -# Author:: Daniel DeLeo (<dan@getchef.com>) -# 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' -require 'tempfile' -require 'chef/workstation_config_loader' - -describe Chef::WorkstationConfigLoader do - - let(:explicit_config_location) { nil } - - let(:env) { {} } - - let(:config_loader) do - described_class.new(explicit_config_location).tap do |c| - allow(c).to receive(:env).and_return(env) - end - end - - # Test methods that do I/O or reference external state which are stubbed out - # elsewhere. - describe "external dependencies" do - let(:config_loader) { described_class.new(nil) } - - it "delegates to ENV for env" do - expect(config_loader.env).to equal(ENV) - end - - it "tests a path's existence" do - expect(config_loader.path_exists?('/nope/nope/nope/nope/frab/jab/nab')).to be(false) - expect(config_loader.path_exists?(__FILE__)).to be(true) - end - - end - - describe "locating the config file" do - context "without an explicit config" do - - before do - allow(config_loader).to receive(:path_exists?).with(an_instance_of(String)).and_return(false) - end - - it "has no config if HOME is not set" do - expect(config_loader.config_location).to be(nil) - expect(config_loader.no_config_found?).to be(true) - end - - context "when HOME is set and contains a knife.rb" do - - let(:home) { "/Users/example.user" } - - before do - env["HOME"] = home - allow(config_loader).to receive(:path_exists?).with("#{home}/.chef/knife.rb").and_return(true) - end - - it "uses the config in HOME/.chef/knife.rb" do - expect(config_loader.config_location).to eq("#{home}/.chef/knife.rb") - end - - context "and has a config.rb" do - - before do - allow(config_loader).to receive(:path_exists?).with("#{home}/.chef/config.rb").and_return(true) - end - - it "uses the config in HOME/.chef/config.rb" do - expect(config_loader.config_location).to eq("#{home}/.chef/config.rb") - end - - context "and/or a parent dir contains a .chef dir" do - - let(:env_pwd) { "/path/to/cwd" } - - before do - if Chef::Platform.windows? - env["CD"] = env_pwd - else - env["PWD"] = env_pwd - end - - allow(config_loader).to receive(:path_exists?).with("#{env_pwd}/.chef/knife.rb").and_return(true) - allow(File).to receive(:exist?).with("#{env_pwd}/.chef").and_return(true) - allow(File).to receive(:directory?).with("#{env_pwd}/.chef").and_return(true) - end - - it "prefers the config from parent_dir/.chef" do - expect(config_loader.config_location).to eq("#{env_pwd}/.chef/knife.rb") - end - - context "and the parent dir's .chef dir has a config.rb" do - - before do - allow(config_loader).to receive(:path_exists?).with("#{env_pwd}/.chef/config.rb").and_return(true) - end - - it "prefers the config from parent_dir/.chef" do - expect(config_loader.config_location).to eq("#{env_pwd}/.chef/config.rb") - end - - context "and/or the current working directory contains a .chef dir" do - - let(:cwd) { Dir.pwd } - - before do - allow(config_loader).to receive(:path_exists?).with("#{cwd}/knife.rb").and_return(true) - end - - it "prefers a knife.rb located in the cwd" do - expect(config_loader.config_location).to eq("#{cwd}/knife.rb") - end - - context "and the CWD's .chef dir has a config.rb" do - - before do - allow(config_loader).to receive(:path_exists?).with("#{cwd}/config.rb").and_return(true) - end - - it "prefers a config located in the cwd" do - expect(config_loader.config_location).to eq("#{cwd}/config.rb") - end - - - context "and/or KNIFE_HOME is set" do - - let(:knife_home) { "/path/to/knife/home" } - - before do - env["KNIFE_HOME"] = knife_home - allow(config_loader).to receive(:path_exists?).with("#{knife_home}/knife.rb").and_return(true) - end - - it "prefers a knife located in KNIFE_HOME" do - expect(config_loader.config_location).to eq("/path/to/knife/home/knife.rb") - end - - context "and KNIFE_HOME contains a config.rb" do - - before do - env["KNIFE_HOME"] = knife_home - allow(config_loader).to receive(:path_exists?).with("#{knife_home}/config.rb").and_return(true) - end - - it "prefers a config.rb located in KNIFE_HOME" do - expect(config_loader.config_location).to eq("/path/to/knife/home/config.rb") - end - - end - - end - end - end - end - end - end - end - - context "when the current working dir is inside a symlinked directory" do - before do - # pwd according to your shell is /home/someuser/prod/chef-repo, but - # chef-repo is a symlink to /home/someuser/codes/chef-repo - env["CD"] = "/home/someuser/prod/chef-repo" # windows - env["PWD"] = "/home/someuser/prod/chef-repo" # unix - - allow(Dir).to receive(:pwd).and_return("/home/someuser/codes/chef-repo") - end - - it "loads the config from the non-dereferenced directory path" do - expect(File).to receive(:exist?).with("/home/someuser/prod/chef-repo/.chef").and_return(false) - expect(File).to receive(:exist?).with("/home/someuser/prod/.chef").and_return(true) - expect(File).to receive(:directory?).with("/home/someuser/prod/.chef").and_return(true) - - expect(config_loader).to receive(:path_exists?).with("/home/someuser/prod/.chef/knife.rb").and_return(true) - - expect(config_loader.config_location).to eq("/home/someuser/prod/.chef/knife.rb") - end - end - end - - context "when given an explicit config to load" do - - let(:explicit_config_location) { "/path/to/explicit/config.rb" } - - it "prefers the explicit config" do - expect(config_loader.config_location).to eq(explicit_config_location) - end - - end - end - - - describe "loading the config file" do - - context "when no explicit config is specifed and no implicit config is found" do - - before do - allow(config_loader).to receive(:path_exists?).with(an_instance_of(String)).and_return(false) - end - - it "skips loading" do - expect(config_loader.config_location).to be(nil) - expect(config_loader.load).to be(false) - end - - end - - context "when an explicit config is given but it doesn't exist" do - - let(:explicit_config_location) { "/nope/nope/nope/frab/jab/nab" } - - it "raises a configuration error" do - expect { config_loader.load }.to raise_error(Chef::Exceptions::ConfigurationError) - end - - end - - context "when the config file exists" do - - let(:config_content) { "" } - - let(:explicit_config_location) do - # could use described_class, but remove all ':' from the path if so. - t = Tempfile.new("Chef-WorkstationConfigLoader-rspec-test") - t.print(config_content) - t.close - t.path - end - - after { File.unlink(explicit_config_location) if File.exists?(explicit_config_location) } - - context "and is valid" do - - let(:config_content) { "config_file_evaluated(true)" } - - it "loads the config" do - expect(config_loader.load).to be(true) - expect(Chef::Config.config_file_evaluated).to be(true) - end - - it "sets Chef::Config.config_file" do - config_loader.load - expect(Chef::Config.config_file).to eq(explicit_config_location) - end - end - - context "and has a syntax error" do - - let(:config_content) { "{{{{{:{{" } - - it "raises a ConfigurationError" do - expect { config_loader.load }.to raise_error(Chef::Exceptions::ConfigurationError) - end - end - - context "and raises a ruby exception during evaluation" do - - let(:config_content) { ":foo\n:bar\nraise 'oops'\n:baz\n" } - - it "raises a ConfigurationError" do - expect { config_loader.load }.to raise_error(Chef::Exceptions::ConfigurationError) - end - end - - end - - end - -end |