summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortylercloke <tylercloke@gmail.com>2015-05-14 14:20:44 -0700
committertylercloke <tylercloke@gmail.com>2015-05-14 14:20:44 -0700
commit38603f0ee0d64e8f5793b1e97bc4f09808d9893e (patch)
tree03439b7fb0ae9044693f3e8d48bbdc3ceee0cc99
parent9a2e18aa8638c08c8c72f8107e520273caa2eabe (diff)
downloadchef-tc/api-version-v1.tar.gz
DEV: for reference, do not merge.tc/api-version-v1
-rw-r--r--lib/chef/api_client.rb215
-rw-r--r--lib/chef/api_client/registration.rb2
-rw-r--r--lib/chef/api_client_v0.rb226
-rw-r--r--lib/chef/api_client_v1.rb200
-rw-r--r--lib/chef/client.rb1
-rw-r--r--lib/chef/exceptions.rb1
-rw-r--r--lib/chef/json_compat.rb9
-rw-r--r--lib/chef/knife/bootstrap/client_builder.rb1
-rw-r--r--lib/chef/mixin/server_api_version.rb37
-rw-r--r--lib/chef/user.rb58
-rw-r--r--spec/unit/api_client_spec.rb332
-rw-r--r--spec/unit/api_client_spec_v0.rb348
-rw-r--r--spec/unit/api_client_spec_v1.rb348
13 files changed, 1264 insertions, 514 deletions
diff --git a/lib/chef/api_client.rb b/lib/chef/api_client.rb
index ce9ceb312c..6de6910dbd 100644
--- a/lib/chef/api_client.rb
+++ b/lib/chef/api_client.rb
@@ -1,7 +1,6 @@
#
-# Author:: Adam Jacob (<adam@opscode.com>)
-# Author:: Nuo Yan (<nuo@opscode.com>)
-# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# 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");
@@ -17,209 +16,37 @@
# limitations under the License.
#
-require 'chef/config'
-require 'chef/mixin/params_validate'
-require 'chef/mixin/from_file'
-require 'chef/mash'
-require 'chef/json_compat'
-require 'chef/search/query'
+require 'chef/exceptions'
+require 'chef/api_client_v0'
+require 'chef/api_client_v1'
class Chef
- class ApiClient
+ # Proxy object for Chef::ApiClientV0 and Chef::ApiClientV1
+ class ApiClient < BasicObject
- include Chef::Mixin::FromFile
- include Chef::Mixin::ParamsValidate
+ SUPPORTED_VERSIONS = [0,1]
- # Create a new Chef::ApiClient object.
- def initialize
- @name = ''
- @public_key = nil
- @private_key = nil
- @admin = false
- @validator = false
- end
-
- # Gets or sets the client name.
- #
- # @params [Optional String] The name must be alpha-numeric plus - and _.
- # @return [String] The current value of the name.
- def name(arg=nil)
- set_or_return(
- :name,
- arg,
- :regex => /^[\-[:alnum:]_\.]+$/
- )
- end
-
- # Gets or sets whether this client is an admin.
- #
- # @params [Optional True/False] Should be true or false - default is false.
- # @return [True/False] The current value
- def admin(arg=nil)
- set_or_return(
- :admin,
- arg,
- :kind_of => [ TrueClass, FalseClass ]
- )
- end
-
- # Gets or sets the public key.
- #
- # @params [Optional String] The string representation of the public key.
- # @return [String] The current value.
- def public_key(arg=nil)
- set_or_return(
- :public_key,
- arg,
- :kind_of => String
- )
- end
-
- # Gets or sets whether this client is a validator.
- #
- # @params [Boolean] whether or not the client is a validator. If
- # `nil`, retrieves the already-set value.
- # @return [Boolean] The current value
- def validator(arg=nil)
- set_or_return(
- :validator,
- arg,
- :kind_of => [TrueClass, FalseClass]
- )
- end
-
- # Gets or sets the private key.
- #
- # @params [Optional String] The string representation of the private key.
- # @return [String] The current value.
- def private_key(arg=nil)
- set_or_return(
- :private_key,
- arg,
- :kind_of => [String, FalseClass]
- )
- end
-
- # The hash representation of the object. Includes the name and public_key.
- # Private key is included if available.
- #
- # @return [Hash]
- def to_hash
- result = {
- "name" => @name,
- "public_key" => @public_key,
- "validator" => @validator,
- "admin" => @admin,
- 'json_class' => self.class.name,
- "chef_type" => "client"
- }
- result["private_key"] = @private_key if @private_key
- result
- end
-
- # The JSON representation of the object.
- #
- # @return [String] the JSON string.
- def to_json(*a)
- Chef::JSONCompat.to_json(to_hash, *a)
- end
-
- def self.from_hash(o)
- client = Chef::ApiClient.new
- client.name(o["name"] || o["clientname"])
- client.private_key(o["private_key"]) if o.key?("private_key")
- client.public_key(o["public_key"])
- client.admin(o["admin"])
- client.validator(o["validator"])
- client
- end
-
- def self.json_create(data)
- from_hash(data)
- end
+ attr_reader :proxy_object
- def self.from_json(j)
- from_hash(Chef::JSONCompat.parse(j))
- end
-
- def self.http_api
- Chef::REST.new(Chef::Config[:chef_server_url])
- end
-
- def self.reregister(name)
- api_client = load(name)
- api_client.reregister
- end
-
- def self.list(inflate=false)
- if inflate
- response = Hash.new
- Chef::Search::Query.new.search(:client) do |n|
- n = self.json_create(n) if n.instance_of?(Hash)
- response[n.name] = n
- end
- response
- else
- http_api.get("clients")
+ def initialize(version=0)
+ unless SUPPORTED_VERSIONS.include?(version)
+ # something about inherting from BasicObject is forcing me to use :: in front of Chef::<whatever>
+ raise ::Chef::Exceptions::InvalidObjectAPIVersionRequested, "You requested Chef::ApiClient version #{version}. Valid versions include #{SUPPORTED_VERSIONS.join(', ')}."
end
- end
- # Load a client by name via the API
- def self.load(name)
- response = http_api.get("clients/#{name}")
- if response.kind_of?(Chef::ApiClient)
- response
- else
- json_create(response)
+ if version == 0
+ @proxy_object = ::Chef::ApiClientV0.new
+ elsif version == 1
+ @proxy_object = ::Chef::ApiClientV1.new
end
end
- # Remove this client via the REST API
- def destroy
- http_api.delete("clients/#{@name}")
- end
-
- # Save this client via the REST API, returns a hash including the private key
- def save
- begin
- http_api.put("clients/#{name}", { :name => self.name, :admin => self.admin, :validator => self.validator})
- rescue Net::HTTPServerException => e
- # If that fails, go ahead and try and update it
- if e.response.code == "404"
- http_api.post("clients", {:name => self.name, :admin => self.admin, :validator => self.validator })
- else
- raise e
- end
- end
- end
-
- def reregister
- reregistered_self = http_api.put("clients/#{name}", { :name => name, :admin => admin, :validator => validator, :private_key => true })
- if reregistered_self.respond_to?(:[])
- private_key(reregistered_self["private_key"])
- else
- private_key(reregistered_self.private_key)
- end
- self
- end
-
- # Create the client via the REST API
- def create
- http_api.post("clients", self)
- end
-
- # As a string
- def to_s
- "client[#{@name}]"
- end
-
- def inspect
- "Chef::ApiClient name:'#{name}' admin:'#{admin.inspect}' validator:'#{validator}' " +
- "public_key:'#{public_key}' private_key:'#{private_key}'"
+ def method_missing(method, *args, &block)
+ @proxy_object.send(method, *args, &block)
end
- def http_api
- @http_api ||= Chef::REST.new(Chef::Config[:chef_server_url])
+ def self.method_missing(method, *arguments, &block)
+ @proxy_class.send(method, *args, &block)
end
end
diff --git a/lib/chef/api_client/registration.rb b/lib/chef/api_client/registration.rb
index de5fc7ac3d..c260ad5834 100644
--- a/lib/chef/api_client/registration.rb
+++ b/lib/chef/api_client/registration.rb
@@ -21,7 +21,7 @@ require 'chef/rest'
require 'chef/exceptions'
class Chef
- class ApiClient
+ class ApiClient < BasicObject
# ==Chef::ApiClient::Registration
# Manages the process of creating or updating a Chef::ApiClient on the
diff --git a/lib/chef/api_client_v0.rb b/lib/chef/api_client_v0.rb
new file mode 100644
index 0000000000..4f0b85ddfe
--- /dev/null
+++ b/lib/chef/api_client_v0.rb
@@ -0,0 +1,226 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Nuo Yan (<nuo@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 'chef/config'
+require 'chef/mixin/params_validate'
+require 'chef/mixin/from_file'
+require 'chef/mash'
+require 'chef/json_compat'
+require 'chef/search/query'
+
+class Chef
+ class ApiClientV0
+
+ include Chef::Mixin::FromFile
+ include Chef::Mixin::ParamsValidate
+
+ # Create a new Chef::ApiClientV0 object.
+ def initialize
+ @name = ''
+ @public_key = nil
+ @private_key = nil
+ @admin = false
+ @validator = false
+ end
+
+ # Gets or sets the client name.
+ #
+ # @params [Optional String] The name must be alpha-numeric plus - and _.
+ # @return [String] The current value of the name.
+ def name(arg=nil)
+ set_or_return(
+ :name,
+ arg,
+ :regex => /^[\-[:alnum:]_\.]+$/
+ )
+ end
+
+ # Gets or sets whether this client is an admin.
+ #
+ # @params [Optional True/False] Should be true or false - default is false.
+ # @return [True/False] The current value
+ def admin(arg=nil)
+ set_or_return(
+ :admin,
+ arg,
+ :kind_of => [ TrueClass, FalseClass ]
+ )
+ end
+
+ # Gets or sets the public key.
+ #
+ # @params [Optional String] The string representation of the public key.
+ # @return [String] The current value.
+ def public_key(arg=nil)
+ set_or_return(
+ :public_key,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ # Gets or sets whether this client is a validator.
+ #
+ # @params [Boolean] whether or not the client is a validator. If
+ # `nil`, retrieves the already-set value.
+ # @return [Boolean] The current value
+ def validator(arg=nil)
+ set_or_return(
+ :validator,
+ arg,
+ :kind_of => [TrueClass, FalseClass]
+ )
+ end
+
+ # Gets or sets the private key.
+ #
+ # @params [Optional String] The string representation of the private key.
+ # @return [String] The current value.
+ def private_key(arg=nil)
+ set_or_return(
+ :private_key,
+ arg,
+ :kind_of => [String, FalseClass]
+ )
+ end
+
+ # The hash representation of the object. Includes the name and public_key.
+ # Private key is included if available.
+ #
+ # @return [Hash]
+ def to_hash
+ result = {
+ "name" => @name,
+ "public_key" => @public_key,
+ "validator" => @validator,
+ "admin" => @admin,
+ 'json_class' => self.class.name,
+ "chef_type" => "client"
+ }
+ result["private_key"] = @private_key if @private_key
+ result
+ end
+
+ # The JSON representation of the object.
+ #
+ # @return [String] the JSON string.
+ def to_json(*a)
+ Chef::JSONCompat.to_json(to_hash, *a)
+ end
+
+ def self.from_hash(o)
+ client = Chef::ApiClientV0.new
+ client.name(o["name"] || o["clientname"])
+ client.private_key(o["private_key"]) if o.key?("private_key")
+ client.public_key(o["public_key"])
+ client.admin(o["admin"])
+ client.validator(o["validator"])
+ client
+ end
+
+ def self.json_create(data)
+ from_hash(data)
+ end
+
+ def self.from_json(j)
+ from_hash(Chef::JSONCompat.parse(j))
+ end
+
+ def self.http_api
+ Chef::REST.new(Chef::Config[:chef_server_url])
+ end
+
+ def self.reregister(name)
+ api_client = load(name)
+ api_client.reregister
+ end
+
+ def self.list(inflate=false)
+ if inflate
+ response = Hash.new
+ Chef::Search::Query.new.search(:client) do |n|
+ n = self.json_create(n) if n.instance_of?(Hash)
+ response[n.name] = n
+ end
+ response
+ else
+ http_api.get("clients")
+ end
+ end
+
+ # Load a client by name via the API
+ def self.load(name)
+ response = http_api.get("clients/#{name}")
+ if response.kind_of?(Chef::ApiClientV0)
+ response
+ else
+ json_create(response)
+ end
+ end
+
+ # Remove this client via the REST API
+ def destroy
+ http_api.delete("clients/#{@name}")
+ end
+
+ # Save this client via the REST API, returns a hash including the private key
+ def save
+ begin
+ http_api.put("clients/#{name}", { :name => self.name, :admin => self.admin, :validator => self.validator})
+ rescue Net::HTTPServerException => e
+ # If that fails, go ahead and try and update it
+ if e.response.code == "404"
+ http_api.post("clients", {:name => self.name, :admin => self.admin, :validator => self.validator })
+ else
+ raise e
+ end
+ end
+ end
+
+ def reregister
+ reregistered_self = http_api.put("clients/#{name}", { :name => name, :admin => admin, :validator => validator, :private_key => true })
+ if reregistered_self.respond_to?(:[])
+ private_key(reregistered_self["private_key"])
+ else
+ private_key(reregistered_self.private_key)
+ end
+ self
+ end
+
+ # Create the client via the REST API
+ def create
+ http_api.post("clients", self)
+ end
+
+ # As a string
+ def to_s
+ "client[#{@name}]"
+ end
+
+ def inspect
+ "Chef::ApiClientV0 name:'#{name}' admin:'#{admin.inspect}' validator:'#{validator}' " +
+ "public_key:'#{public_key}' private_key:'#{private_key}'"
+ end
+
+ def http_api
+ @http_api ||= Chef::REST.new(Chef::Config[:chef_server_url])
+ end
+
+ end
+end
diff --git a/lib/chef/api_client_v1.rb b/lib/chef/api_client_v1.rb
new file mode 100644
index 0000000000..868acd9791
--- /dev/null
+++ b/lib/chef/api_client_v1.rb
@@ -0,0 +1,200 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Nuo Yan (<nuo@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 'chef/config'
+require 'chef/mixin/params_validate'
+require 'chef/mixin/from_file'
+require 'chef/mash'
+require 'chef/json_compat'
+require 'chef/search/query'
+
+class Chef
+ class ApiClientV1
+
+ include Chef::Mixin::FromFile
+ include Chef::Mixin::ParamsValidate
+
+ # Create a new Chef::ApiClientV1 object.
+ def initialize
+ @name = ''
+ @private_key = nil
+ @create_key = nil
+ @validator = false
+ end
+
+ # Gets or sets the client name.
+ #
+ # @params [Optional String] The name must be alpha-numeric plus - and _.
+ # @return [String] The current value of the name.
+ def name(arg=nil)
+ set_or_return(
+ :name,
+ arg,
+ :regex => /^[\-[:alnum:]_\.]+$/
+ )
+ end
+
+ # Gets or sets whether this client is a validator.
+ #
+ # @params [Boolean] whether or not the client is a validator. If
+ # `nil`, retrieves the already-set value.
+ # @return [Boolean] The current value
+ def validator(arg=nil)
+ set_or_return(
+ :validator,
+ arg,
+ :kind_of => [TrueClass, FalseClass]
+ )
+ end
+
+ # Field to ask server to create a "default" public / private key pair
+ # on initial client POST. Only valid / relevant on initial POST.
+ def create_key(arg=nil)
+ set_or_return(:create_key, arg,
+ :kind_of => [TrueClass, FalseClass])
+ end
+
+ # Gets or sets the private key.
+ #
+ # @params [Optional String] The string representation of the private key.
+ # @return [String] The current value.
+ def private_key(arg=nil)
+ set_or_return(
+ :private_key,
+ arg,
+ :kind_of => [String, FalseClass]
+ )
+ end
+
+ # The hash representation of the object. Includes the name.
+ # Private key is included if available.
+ #
+ # @return [Hash]
+ def to_hash
+ result = {
+ "name" => @name,
+ "validator" => @validator
+ }
+ result["private_key"] = @private_key if @private_key
+ result
+ end
+
+ # The JSON representation of the object.
+ #
+ # @return [String] the JSON string.
+ def to_json(*a)
+ Chef::JSONCompat.to_json(to_hash, *a)
+ end
+
+ def self.from_hash(o)
+ client = Chef::ApiClientV1.new
+ client.name(o["name"] || o["clientname"])
+ client.private_key(o["private_key"]) if o.key?("private_key")
+ client.validator(o["validator"])
+ client
+ end
+
+ def self.json_create(data)
+ from_hash(data)
+ end
+
+ def self.from_json(j)
+ from_hash(Chef::JSONCompat.parse(j))
+ end
+
+ def self.http_api
+ Chef::REST.new(Chef::Config[:chef_server_url])
+ end
+
+ def self.list(inflate=false)
+ if inflate
+ response = Hash.new
+ Chef::Search::Query.new.search(:client) do |n|
+ n = self.json_create(n) if n.instance_of?(Hash)
+ response[n.name] = n
+ end
+ response
+ else
+ http_api.get("clients")
+ end
+ end
+
+ # Load a client by name via the API
+ def self.load(name)
+ response = http_api.get("clients/#{name}")
+ if response.kind_of?(Chef::ApiClientV1)
+ response
+ else
+ json_create(response)
+ end
+ end
+
+ # Remove this client via the REST API
+ def destroy
+ http_api.delete("clients/#{@name}")
+ end
+
+ # Save this client via the REST API, returns a hash including the private key
+ def save
+ begin
+ http_api.put("clients/#{name}", {:name => self.name, :validator => self.validator})
+ rescue Net::HTTPServerException => e
+ # If that fails, go ahead and try and update it
+ if e.response.code == "404"
+ http_api.post("clients", {:name => self.name, :validator => self.validator })
+ else
+ raise e
+ end
+ end
+ end
+
+ # Create the client via the REST API
+ def create(initial_public_key = nil)
+ payload = {:name => @name, :validator => @validator}
+ raise Chef::Exceptions::InvalidClientAttribute, "you cannot pass an initial_public_key to create if create_key is true" if @create_key && initial_public_key
+ payload[:public_key] = initial_public_key if initial_public_key
+ payload[:create_key] = @create_key if @create_key
+ new_client = Chef::REST.new(Chef::Config[:chef_server_url]).post_rest("clients", payload)
+
+ # get the private_key out of the chef_key hash if it exists
+ if new_client['chef_key']
+ if new_client['chef_key']['private_key']
+ new_client['private_key'] = new_client['chef_key']['private_key']
+ end
+ delete new_client['chef_key']
+ end
+ Chef::User.from_hash(self.to_hash.merge(new_client))
+ end
+
+ # As a string
+ def to_s
+ "client[#{@name}]"
+ end
+
+ def inspect
+ string = "Chef::ApiClientV1 name:'#{@name}' validator:'#{@validator}' "
+ string = "private_key:'#{@private_key}'" if @private_key
+ end
+
+ def http_api
+ @http_api ||= Chef::REST.new(Chef::Config[:chef_server_url])
+ end
+
+ end
+end
diff --git a/lib/chef/client.rb b/lib/chef/client.rb
index 0764d3f3ba..c821e8684b 100644
--- a/lib/chef/client.rb
+++ b/lib/chef/client.rb
@@ -23,7 +23,6 @@ require 'chef/mixin/params_validate'
require 'chef/mixin/path_sanity'
require 'chef/log'
require 'chef/rest'
-require 'chef/api_client'
require 'chef/api_client/registration'
require 'chef/audit/runner'
require 'chef/node'
diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb
index 736d9fd6a4..aa6f798711 100644
--- a/lib/chef/exceptions.rb
+++ b/lib/chef/exceptions.rb
@@ -97,6 +97,7 @@ class Chef
class InvalidResourceReference < RuntimeError; end
class ResourceNotFound < RuntimeError; end
class VerificationNotFound < RuntimeError; end
+ class InvalidObjectAPIVersionRequested < ArgumentError; end
# Can't find a Resource of this type that is valid on this platform.
class NoSuchResourceType < NameError
diff --git a/lib/chef/json_compat.rb b/lib/chef/json_compat.rb
index d0b3b4c7f8..a8d9e6183c 100644
--- a/lib/chef/json_compat.rb
+++ b/lib/chef/json_compat.rb
@@ -28,7 +28,8 @@ class Chef
JSON_CLASS = "json_class".freeze
- CHEF_APICLIENT = "Chef::ApiClient".freeze
+ CHEF_APICLIENTV0 = "Chef::ApiClientV0".freeze
+ CHEF_APICLIENTV1 = "Chef::ApiClientV1".freeze
CHEF_CHECKSUM = "Chef::Checksum".freeze
CHEF_COOKBOOKVERSION = "Chef::CookbookVersion".freeze
CHEF_DATABAG = "Chef::DataBag".freeze
@@ -122,8 +123,10 @@ class Chef
# the world to get json, which would make knife very slow.
def class_for_json_class(json_class)
case json_class
- when CHEF_APICLIENT
- Chef::ApiClient
+ when CHEF_APICLIENTV0
+ Chef::ApiClientV0
+ when CHEF_APICLIENTV1
+ Chef::ApiClientV1
when CHEF_CHECKSUM
Chef::Checksum
when CHEF_COOKBOOKVERSION
diff --git a/lib/chef/knife/bootstrap/client_builder.rb b/lib/chef/knife/bootstrap/client_builder.rb
index b9c1d98bec..6f5a045800 100644
--- a/lib/chef/knife/bootstrap/client_builder.rb
+++ b/lib/chef/knife/bootstrap/client_builder.rb
@@ -19,7 +19,6 @@
require 'chef/node'
require 'chef/rest'
require 'chef/api_client/registration'
-require 'chef/api_client'
require 'tmpdir'
class Chef
diff --git a/lib/chef/mixin/server_api_version.rb b/lib/chef/mixin/server_api_version.rb
new file mode 100644
index 0000000000..714859bd71
--- /dev/null
+++ b/lib/chef/mixin/server_api_version.rb
@@ -0,0 +1,37 @@
+#
+# Author:: Tyler Cloke (<tyler@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 'chef/exceptions'
+
+class Chef
+ module Mixin
+ module ServerApiVersion
+
+
+ # Input: server_api_version should be a string of an integer
+ def handle_request_api_version(server_api_version)
+ # raise Chef::Exceptions::InvalidCommandOption if the server_api_version requested is not supported by the client
+ server_api_version = config[:server_api_version].to_s
+ supported_versions = Chef::REST::SUPPORTED_SERVER_API_VERSIONS
+ unless supported_versions.include? config[:server_api_version].to_s
+ raise Chef::Exceptions::InvalidCommandOption "You requested a server API version of #{server_api_version} via --server-api version. This version of the Chef client only supports supported_versions.join(', ')."
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/user.rb b/lib/chef/user.rb
new file mode 100644
index 0000000000..22a9261cf6
--- /dev/null
+++ b/lib/chef/user.rb
@@ -0,0 +1,58 @@
+#
+# 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 'chef/exceptions'
+require 'chef/user_v0'
+require 'chef/user_v1'
+
+class Chef
+ # Proxy object for Chef::UserV0 and Chef::UserV1
+ class User < BasicObject
+
+ SUPPORTED_VERSIONS = [0,1]
+
+ attr_reader :proxy_object
+
+ def initialize(version=0)
+ unless SUPPORTED_VERSIONS.include?(version)
+ # something about inherting from BasicObject is forcing me to use :: in front of Chef::<whatever>
+ raise ::Chef::Exceptions::InvalidObjectAPIVersionRequested, "You requested Chef::User version #{version}. Valid versions include #{SUPPORTED_VERSIONS.join(', ')}."
+ end
+
+ if version == 0
+ @proxy_class = ::Chef::UserV0
+ @proxy_object = @proxy_class.new
+ elsif version == 1
+ @proxy_class = ::Chef::UserV1
+ @proxy_object = @proxy_class.new
+ end
+ end
+
+ def method_missing(method, *args, &block)
+ @proxy_object.send(method, *args, &block)
+ end
+
+ def self.method_missing(method, *arguments, &block)
+ puts "halp"*100
+ puts method
+ puts @proxy_class.class
+ @proxy_class.send(method, *args, &block)
+ end
+
+ end
+end
diff --git a/spec/unit/api_client_spec.rb b/spec/unit/api_client_spec.rb
index 7668e31f5a..0586c54c01 100644
--- a/spec/unit/api_client_spec.rb
+++ b/spec/unit/api_client_spec.rb
@@ -1,6 +1,6 @@
#
-# Author:: Adam Jacob (<adam@opscode.com>)
-# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# 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");
@@ -19,330 +19,34 @@
require 'spec_helper'
require 'chef/api_client'
-require 'tempfile'
describe Chef::ApiClient do
- before(:each) do
- @client = Chef::ApiClient.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 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 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 equalivent to Chef::JSONCompat.to_json" do
- let(:jsonable) { @client }
+ context "when Chef::ApiClient is called with an invalid version" do
+ it "raises a Chef::Exceptions::InvalidObjectAPIVersionRequested" do
+ expect{ Chef::ApiClient.new(-100) }.to raise_error(Chef::Exceptions::InvalidObjectAPIVersionRequested)
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}"
- end
-
- let(:client) do
- Chef::ApiClient.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::ApiClient object" do
- expect(client).to be_a_kind_of(Chef::ApiClient)
- 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 '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")
+ context "when Chef::ApiClient is called with the default version" do
+ it "properly creates the default Chef::ApiClient versioned object" do
+ object = Chef::ApiClient.new
+ expect(object.proxy_object.class).to eq(Chef::ApiClientV0)
end
end
- describe "when deserializing from JSON (hash) using JSONCompat#from_json" do
- let(:client_hash) do
- {
- "name" => "black",
- "public_key" => "crowes",
- "private_key" => "monkeypants",
- "admin" => true,
- "validator" => true,
- "json_class" => "Chef::ApiClient"
- }
- end
-
- let(:client) do
- Chef::JSONCompat.from_json(Chef::JSONCompat.to_json(client_hash))
- end
-
- it "should deserialize to a Chef::ApiClient object" do
- expect(client).to be_a_kind_of(Chef::ApiClient)
- 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 '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")
+ context "when Chef::ApiClient is called with a non-default version" do
+ it "properly creates the correct Chef::ApiClient versioned object" do
+ object = Chef::ApiClient.new(1)
+ expect(object.proxy_object.class).to eq(Chef::ApiClientV1)
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,
- "validator" => true,
- "json_class" => "Chef::ApiClient"
- }
- @http_client = double("Chef::REST mock")
- allow(Chef::REST).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
-
- it "should deserialize to a Chef::ApiClient object" do
- expect(@client).to be_a_kind_of(Chef::ApiClient)
- 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 '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
-
- 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)
+ context "when Chef::ApiClient is called with a non-default vasdfersion" do
+ it "properly creates the correct Chef::ApiClient versionasdfed object" do
+ object = Chef::ApiClient.new(1)
+ expect(object.proxy_class).to eq(Chef::ApiClientV1)
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)
- 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::ApiClient.reregister("lost-my-key") }.to raise_error(Net::HTTPServerException)
- end
- end
-
- context "and the client exists" do
- before do
- @api_client_without_key = Chef::ApiClient.new
- @api_client_without_key.name("lost-my-key")
- expect(@http_client).to receive(:get).with("clients/lost-my-key").and_return(@api_client_without_key)
- end
-
-
- context "and the client exists on a Chef 11-like server" do
- before do
- @api_client_with_key = Chef::ApiClient.new
- @api_client_with_key.name("lost-my-key")
- @api_client_with_key.private_key("the new private key")
- expect(@http_client).to receive(:put).
- with("clients/lost-my-key", :name => "lost-my-key", :admin => false, :validator => false, :private_key => true).
- and_return(@api_client_with_key)
- end
-
- it "returns an ApiClient with a private key" do
- response = Chef::ApiClient.reregister("lost-my-key")
- # no sane == method for ApiClient :'(
- expect(response).to eq(@api_client_without_key)
- expect(response.private_key).to eq("the new private key")
- expect(response.name).to eq("lost-my-key")
- expect(response.admin).to be_falsey
- end
- end
-
- context "and the client exists on a Chef 10-like server" do
- before do
- @api_client_with_key = {"name" => "lost-my-key", "private_key" => "the new private key"}
- expect(@http_client).to receive(:put).
- with("clients/lost-my-key", :name => "lost-my-key", :admin => false, :validator => false, :private_key => true).
- and_return(@api_client_with_key)
- end
-
- it "returns an ApiClient with a private key" do
- response = Chef::ApiClient.reregister("lost-my-key")
- # no sane == method for ApiClient :'(
- expect(response).to eq(@api_client_without_key)
- expect(response.private_key).to eq("the new private key")
- expect(response.name).to eq("lost-my-key")
- expect(response.admin).to be_falsey
- expect(response.validator).to be_falsey
- end
- end
-
- end
- end
end
+
diff --git a/spec/unit/api_client_spec_v0.rb b/spec/unit/api_client_spec_v0.rb
new file mode 100644
index 0000000000..3dd5b633b0
--- /dev/null
+++ b/spec/unit/api_client_spec_v0.rb
@@ -0,0 +1,348 @@
+#
+# 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_v0'
+require 'tempfile'
+
+describe Chef::ApiClientV0 do
+ before(:each) do
+ @client = Chef::ApiClientV0.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 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 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 equalivent 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}"
+ end
+
+ let(:client) do
+ Chef::ApiClientV0.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::ApiClientV0 object" do
+ expect(client).to be_a_kind_of(Chef::ApiClientV0)
+ 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 '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 JSONCompat#from_json" do
+ let(:client_hash) do
+ {
+ "name" => "black",
+ "public_key" => "crowes",
+ "private_key" => "monkeypants",
+ "admin" => true,
+ "validator" => true,
+ "json_class" => "Chef::ApiClientV0"
+ }
+ end
+
+ let(:client) do
+ Chef::JSONCompat.from_json(Chef::JSONCompat.to_json(client_hash))
+ end
+
+ it "should deserialize to a Chef::ApiClientV0 object" do
+ expect(client).to be_a_kind_of(Chef::ApiClientV0)
+ 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 '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,
+ "validator" => true,
+ "json_class" => "Chef::ApiClientV0"
+ }
+ @http_client = double("Chef::REST mock")
+ allow(Chef::REST).to receive(:new).and_return(@http_client)
+ expect(@http_client).to receive(:get).with("clients/black").and_return(client)
+ @client = Chef::ApiClientV0.load(client['name'])
+ end
+
+ it "should deserialize to a Chef::ApiClientV0 object" do
+ expect(@client).to be_a_kind_of(Chef::ApiClientV0)
+ 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 '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
+
+ 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)
+ 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::ApiClientV0.reregister("lost-my-key") }.to raise_error(Net::HTTPServerException)
+ end
+ end
+
+ context "and the client exists" do
+ before do
+ @api_client_without_key = Chef::ApiClientV0.new
+ @api_client_without_key.name("lost-my-key")
+ expect(@http_client).to receive(:get).with("clients/lost-my-key").and_return(@api_client_without_key)
+ end
+
+
+ context "and the client exists on a Chef 11-like server" do
+ before do
+ @api_client_with_key = Chef::ApiClientV0.new
+ @api_client_with_key.name("lost-my-key")
+ @api_client_with_key.private_key("the new private key")
+ expect(@http_client).to receive(:put).
+ with("clients/lost-my-key", :name => "lost-my-key", :admin => false, :validator => false, :private_key => true).
+ and_return(@api_client_with_key)
+ end
+
+ it "returns an ApiClient with a private key" do
+ response = Chef::ApiClientV0.reregister("lost-my-key")
+ # no sane == method for ApiClient :'(
+ expect(response).to eq(@api_client_without_key)
+ expect(response.private_key).to eq("the new private key")
+ expect(response.name).to eq("lost-my-key")
+ expect(response.admin).to be_falsey
+ end
+ end
+
+ context "and the client exists on a Chef 10-like server" do
+ before do
+ @api_client_with_key = {"name" => "lost-my-key", "private_key" => "the new private key"}
+ expect(@http_client).to receive(:put).
+ with("clients/lost-my-key", :name => "lost-my-key", :admin => false, :validator => false, :private_key => true).
+ and_return(@api_client_with_key)
+ end
+
+ it "returns an ApiClient with a private key" do
+ response = Chef::ApiClientV0.reregister("lost-my-key")
+ # no sane == method for ApiClient :'(
+ expect(response).to eq(@api_client_without_key)
+ expect(response.private_key).to eq("the new private key")
+ expect(response.name).to eq("lost-my-key")
+ expect(response.admin).to be_falsey
+ expect(response.validator).to be_falsey
+ end
+ end
+
+ end
+ end
+end
diff --git a/spec/unit/api_client_spec_v1.rb b/spec/unit/api_client_spec_v1.rb
new file mode 100644
index 0000000000..d034f4bf02
--- /dev/null
+++ b/spec/unit/api_client_spec_v1.rb
@@ -0,0 +1,348 @@
+#
+# 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'
+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 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 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 equalivent 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}"
+ 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 '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 JSONCompat#from_json" do
+ let(:client_hash) do
+ {
+ "name" => "black",
+ "public_key" => "crowes",
+ "private_key" => "monkeypants",
+ "admin" => true,
+ "validator" => true,
+ "json_class" => "Chef::ApiClientV1"
+ }
+ end
+
+ let(:client) do
+ Chef::JSONCompat.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 '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,
+ "validator" => true,
+ "json_class" => "Chef::ApiClientV1"
+ }
+ @http_client = double("Chef::REST mock")
+ allow(Chef::REST).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 '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
+
+ 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)
+ 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
+
+ context "and the client exists" do
+ before do
+ @api_client_without_key = Chef::ApiClientV1.new
+ @api_client_without_key.name("lost-my-key")
+ expect(@http_client).to receive(:get).with("clients/lost-my-key").and_return(@api_client_without_key)
+ end
+
+
+ context "and the client exists on a Chef 11-like server" do
+ before do
+ @api_client_with_key = Chef::ApiClientV1.new
+ @api_client_with_key.name("lost-my-key")
+ @api_client_with_key.private_key("the new private key")
+ expect(@http_client).to receive(:put).
+ with("clients/lost-my-key", :name => "lost-my-key", :admin => false, :validator => false, :private_key => true).
+ and_return(@api_client_with_key)
+ end
+
+ it "returns an ApiClient with a private key" do
+ response = Chef::ApiClientV1.reregister("lost-my-key")
+ # no sane == method for ApiClient :'(
+ expect(response).to eq(@api_client_without_key)
+ expect(response.private_key).to eq("the new private key")
+ expect(response.name).to eq("lost-my-key")
+ expect(response.admin).to be_falsey
+ end
+ end
+
+ context "and the client exists on a Chef 10-like server" do
+ before do
+ @api_client_with_key = {"name" => "lost-my-key", "private_key" => "the new private key"}
+ expect(@http_client).to receive(:put).
+ with("clients/lost-my-key", :name => "lost-my-key", :admin => false, :validator => false, :private_key => true).
+ and_return(@api_client_with_key)
+ end
+
+ it "returns an ApiClient with a private key" do
+ response = Chef::ApiClientV1.reregister("lost-my-key")
+ # no sane == method for ApiClient :'(
+ expect(response).to eq(@api_client_without_key)
+ expect(response.private_key).to eq("the new private key")
+ expect(response.name).to eq("lost-my-key")
+ expect(response.admin).to be_falsey
+ expect(response.validator).to be_falsey
+ end
+ end
+
+ end
+ end
+end