berkshelf 0.3.7 → 0.4.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. data/.gitignore +2 -1
  2. data/README.md +8 -0
  3. data/Thorfile +2 -2
  4. data/berkshelf.gemspec +1 -1
  5. data/features/install.feature +102 -2
  6. data/features/lockfile.feature +1 -1
  7. data/features/step_definitions/chef_server_steps.rb +8 -0
  8. data/features/step_definitions/cli_steps.rb +1 -1
  9. data/features/step_definitions/filesystem_steps.rb +12 -0
  10. data/features/support/env.rb +8 -10
  11. data/features/update.feature +1 -1
  12. data/lib/berkshelf.rb +19 -1
  13. data/lib/berkshelf/berksfile.rb +18 -6
  14. data/lib/berkshelf/cli.rb +3 -8
  15. data/lib/berkshelf/cookbook_source.rb +108 -23
  16. data/lib/berkshelf/cookbook_source/chef_api_location.rb +256 -0
  17. data/lib/berkshelf/cookbook_source/git_location.rb +14 -2
  18. data/lib/berkshelf/cookbook_source/location.rb +119 -2
  19. data/lib/berkshelf/cookbook_source/path_location.rb +6 -1
  20. data/lib/berkshelf/cookbook_source/site_location.rb +36 -105
  21. data/lib/berkshelf/cookbook_store.rb +7 -12
  22. data/lib/berkshelf/dsl.rb +1 -1
  23. data/lib/berkshelf/errors.rb +2 -0
  24. data/lib/berkshelf/init_generator.rb +6 -6
  25. data/lib/berkshelf/resolver.rb +8 -34
  26. data/lib/berkshelf/uploader.rb +7 -7
  27. data/lib/berkshelf/version.rb +1 -1
  28. data/spec/fixtures/reset.pem +27 -0
  29. data/spec/knife.rb.sample +12 -0
  30. data/spec/spec_helper.rb +4 -1
  31. data/spec/support/chef_api.rb +32 -0
  32. data/spec/support/knife.rb +18 -0
  33. data/spec/unit/berkshelf/cookbook_source/chef_api_location_spec.rb +243 -0
  34. data/spec/unit/berkshelf/cookbook_source/git_location_spec.rb +2 -2
  35. data/spec/unit/berkshelf/cookbook_source/location_spec.rb +130 -2
  36. data/spec/unit/berkshelf/cookbook_source/path_location_spec.rb +2 -2
  37. data/spec/unit/berkshelf/cookbook_source/site_location_spec.rb +22 -105
  38. data/spec/unit/berkshelf/cookbook_source_spec.rb +140 -71
  39. data/spec/unit/berkshelf/cookbook_store_spec.rb +6 -6
  40. data/spec/unit/berkshelf/resolver_spec.rb +9 -30
  41. data/spec/unit/berkshelf/uploader_spec.rb +1 -10
  42. data/spec/unit/berkshelf_spec.rb +21 -1
  43. metadata +19 -15
  44. data/features/config.sample.yml +0 -3
@@ -3,9 +3,6 @@ require 'fileutils'
3
3
  module Berkshelf
4
4
  # @author Jamie Winsor <jamie@vialstudios.com>
5
5
  class CookbookStore
6
- include DepSelector
7
- include DepSelector::Exceptions
8
-
9
6
  attr_reader :storage_path
10
7
 
11
8
  # Create a new instance of CookbookStore with the given
@@ -69,27 +66,25 @@ module Berkshelf
69
66
  # constraint. Nil is returned if no matching CachedCookbook is found.
70
67
  #
71
68
  # @param [#to_s] name
72
- # @param [DepSelector::VersionConstraint] constraint
69
+ # @param [Solve::Constraint] constraint
73
70
  #
74
71
  # @return [Berkshelf::CachedCookbook, nil]
75
72
  def satisfy(name, constraint)
76
- graph = DependencyGraph.new
77
- selector = Selector.new(graph)
78
- package = graph.package(name)
79
- solution_constraints = [ SolutionConstraint.new(graph.package(name), constraint) ]
73
+ graph = Solve::Graph.new
74
+ cookbooks(name).each { |cookbook| graph.artifacts(name, cookbook.version) }
75
+ graph.demands(name, constraint)
80
76
 
81
- cookbooks(name).each { |cookbook| package.add_version(Version.new(cookbook.version)) }
82
- name, version = quietly { selector.find_solution(solution_constraints).first }
77
+ name, version = Solve.it!(graph).first
83
78
 
84
79
  cookbook(name, version)
85
- rescue InvalidSolutionConstraints, NoSolutionExists
80
+ rescue Solve::NoSolutionError
86
81
  nil
87
82
  end
88
83
 
89
84
  private
90
85
 
91
86
  def initialize_filesystem
92
- FileUtils.mkdir_p(storage_path, :mode => 0755)
87
+ FileUtils.mkdir_p(storage_path, mode: 0755)
93
88
  end
94
89
  end
95
90
  end
@@ -32,7 +32,7 @@ module Berkshelf
32
32
  metadata.name
33
33
  end
34
34
 
35
- source = CookbookSource.new(name, :path => File.dirname(metadata_file))
35
+ source = CookbookSource.new(name, path: File.dirname(metadata_file))
36
36
  add_source(source)
37
37
  end
38
38
  end
@@ -11,6 +11,7 @@ module Berkshelf
11
11
  alias_method :message, :to_s
12
12
  end
13
13
 
14
+ class InternalError < BerkshelfError; status_code(99); end
14
15
  class BerksfileNotFound < BerkshelfError; status_code(100); end
15
16
  class NoVersionForConstraints < BerkshelfError; status_code(101); end
16
17
  class DownloadFailure < BerkshelfError; status_code(102); end
@@ -58,4 +59,5 @@ module Berkshelf
58
59
  end
59
60
 
60
61
  class ConstraintNotSatisfied < BerkshelfError; status_code(111); end
62
+ class InvalidChefAPILocation < BerkshelfError; status_code(112); end
61
63
  end
@@ -12,16 +12,16 @@ module Berkshelf
12
12
  include Thor::Actions
13
13
 
14
14
  argument :path,
15
- :type => :string,
16
- :required => true
15
+ type: :string,
16
+ required: true
17
17
 
18
18
  class_option :metadata_entry,
19
- :type => :boolean,
20
- :default => false
19
+ type: :boolean,
20
+ default: false
21
21
 
22
22
  class_option :chefignore,
23
- :type => :boolean,
24
- :default => false
23
+ type: :boolean,
24
+ default: false
25
25
 
26
26
  def generate
27
27
  target_path = File.expand_path(path)
@@ -2,16 +2,14 @@ module Berkshelf
2
2
  # @author Jamie Winsor <jamie@vialstudios.com>
3
3
  class Resolver
4
4
  extend Forwardable
5
- include DepSelector
6
5
 
7
- def_delegator :@graph, :package
8
- def_delegator :@graph, :packages
6
+ attr_reader :graph
9
7
 
10
8
  # @param [Downloader] downloader
11
9
  # @param [Array<CookbookSource>, CookbookSource] sources
12
10
  def initialize(downloader, sources = Array.new)
13
11
  @downloader = downloader
14
- @graph = DependencyGraph.new
12
+ @graph = Solve::Graph.new
15
13
  @sources = Hash.new
16
14
 
17
15
  # Dependencies need to be added AFTER the sources. If they are
@@ -46,8 +44,7 @@ module Berkshelf
46
44
  set_source(source)
47
45
  use_source(source) || install_source(source)
48
46
 
49
- package = add_package(source.name)
50
- package_version = add_version(package, Version.new(source.cached_cookbook.version))
47
+ graph.artifacts(source.name, source.cached_cookbook.version)
51
48
 
52
49
  if include_dependencies
53
50
  add_source_dependencies(source)
@@ -84,7 +81,11 @@ module Berkshelf
84
81
  #
85
82
  # @return [Array<Berkshelf::CachedCookbook>]
86
83
  def resolve
87
- solution = quietly { selector.find_solution(solution_constraints) }
84
+ graph.artifacts.each do |artifact|
85
+ graph.demands(artifact.name)
86
+ end
87
+
88
+ solution = Solve.it!(graph)
88
89
 
89
90
  [].tap do |cached_cookbooks|
90
91
  solution.each do |name, version|
@@ -114,7 +115,6 @@ module Berkshelf
114
115
  private
115
116
 
116
117
  attr_reader :downloader
117
- attr_reader :graph
118
118
 
119
119
  # @param [CookbookSource] source
120
120
  def set_source(source)
@@ -164,31 +164,5 @@ module Berkshelf
164
164
 
165
165
  true
166
166
  end
167
-
168
- def selector
169
- Selector.new(graph)
170
- end
171
-
172
- def solution_constraints
173
- constraints = graph.packages.collect do |name, package|
174
- SolutionConstraint.new(package)
175
- end
176
- end
177
-
178
- # @param [String] name
179
- # name of the package to add to the graph
180
- def add_package(name)
181
- graph.package(name)
182
- end
183
-
184
- # Add a version to a package
185
- #
186
- # @param [DepSelector::Package] package
187
- # the package to add a version to
188
- # @param [DepSelector::Version] version
189
- # the version to add the the package
190
- def add_version(package, version)
191
- package.add_version(version)
192
- end
193
167
  end
194
168
  end
@@ -71,7 +71,7 @@ module Berkshelf
71
71
  memo
72
72
  end
73
73
 
74
- rest.post_rest("sandboxes", :checksums => massaged_sums)
74
+ rest.post_rest("sandboxes", checksums: massaged_sums)
75
75
  end
76
76
 
77
77
  def commit_sandbox(sandbox)
@@ -107,13 +107,13 @@ module Berkshelf
107
107
  file_contents = File.open(file, "rb") {|f| f.read}
108
108
 
109
109
  sign_obj = Mixlib::Authentication::SignedHeaderAuth.signing_object(
110
- :http_method => :put,
111
- :path => URI.parse(url).path,
112
- :body => file_contents,
113
- :timestamp => timestamp,
114
- :user_id => rest.client_name
110
+ http_method: :put,
111
+ path: URI.parse(url).path,
112
+ body: file_contents,
113
+ timestamp: timestamp,
114
+ user_id: rest.client_name
115
115
  )
116
- headers = { 'content-type' => 'application/x-binary', 'content-md5' => checksum64, :accept => 'application/json' }
116
+ headers = { 'content-type' => 'application/x-binary', 'content-md5' => checksum64, accept: 'application/json' }
117
117
  headers.merge!(sign_obj.sign(OpenSSL::PKey::RSA.new(rest.signing_key)))
118
118
 
119
119
  begin
@@ -1,3 +1,3 @@
1
1
  module Berkshelf
2
- VERSION = "0.3.7"
2
+ VERSION = "0.4.0.rc1"
3
3
  end
@@ -0,0 +1,27 @@
1
+ -----BEGIN RSA PRIVATE KEY-----
2
+ MIIEpQIBAAKCAQEAyyUMqrTh1IzKOyE0fvXEWC7m0AdMI8/dr9JJMUKtK9vhhP0w
3
+ rm6m95GoybFM2IRryukFsAxpcir3M1ungTU3Smq4MshhMJ7H9FbvZVfQoknTbCsR
4
+ w6scg2fBepxT2+fcGRufr8nAh92M3uUkN9bMMTAkt18D4br6035YvdmvHDJERxYq
5
+ ByA/720AdI9VNSIvw+x8oqsIkXLEdF6dgT9MpG5iWZT66pbFsnNZpRrd4/bFNWBY
6
+ +13aOqdmjiTL08/EdgQFKMT5qimpos1TuQhA7mwInOjQgzVu9uCDkMiYejaLbUz0
7
+ lGyS8y4uxu6z2hA900Jg/z+JJuXymH5QAX3GZQIDAQABAoIBAQCtFXkwbYPI1Nht
8
+ /wG6du5+8B9K+hy+mppY9wPTy+q+Zs9Ev3Fd/fuXDm1QxBckl9c8AMUO1dR2KPOM
9
+ t7gFl/DvH/SnmCFvCqp1nijFIUgrLlnMXPn6zG0z7RBlxpKQ2IGohufNIEpBuNwR
10
+ Ag2U4hgChPGTp4ooJ2cVEh7MS5AupYPDbC62dWEdW68aRTWhh2BCGAWBb6s16yl9
11
+ aZ7+OcxW2eeRJVbRfLkLQEDutJZi5TfOEn5QPc86ZgxcCmnvwulnpnhpz6QCkgQt
12
+ OP/+KRqDhWSDVCFREVT30fUIj1EWvK7NFWASZQxueZStuIvMEKeFebYfrbHxRFzJ
13
+ UmaxJnWVAoGBAPbKLpeky6ClccBaHHrCgjzakoDfGgyNKDQ9g753lJxB8nn7d9X4
14
+ HQpkWpfqAGFRZp1hI2H+VxyUXLh2Ob5OUeTm0OZJll35vycOaQEtfgIScXTcvzn0
15
+ 16J9eX2YY4wIHEEMh85nKk8BEGgiNP5nuEviHocCeYXoi/Zq3+qj6v63AoGBANK5
16
+ 4nyi6LBQFs1CUc7Sh7vjtOE3ia7KeRmOr7gS6QhS3iK3Oa8FzBLJ6ETjN2a9Bw8N
17
+ cF7I/+cr4s7DUJjxdb53D/J6TVSYORNNCUVnpF/uB2LqqdXDYmpO0PvFkXFoYTnJ
18
+ kaLAN8uCoLKr6JH9tq3DfXIfDIHiZ+BOIvI070fDAoGBAMDyzEDFmGruTyRLj66u
19
+ +rJnVVmqlKwxhLhrS+CTj74nlVOnt0a0KMhiM65IRqnPwcHUG5zXBPaUTHXwAS93
20
+ /nFPwQ37hLPOupPnoVNJZRZrowbyPBQtCJbDMURv64ylHqoBCQDoCd0hANnZvMMX
21
+ BrFVhfaaibaXXS542r6SD/27AoGAECadHE5kJTdOOBcwK/jo3Fa8g1J9Y/8yvum3
22
+ wBT69V9clS6T5j08geglvDnqAh7UzquKBEnFi1NKw+wmXkKLcrivaTdEfApavYb3
23
+ AfHKoGue907jC3Y5Mcquq81ds2J7qTEwz1eKLzfo1yjj32ShvrmwALIuhDn1GjUC
24
+ 6qtx938CgYEApEqvu0nocR1jmVVlLe5uKQBj949dh6NGq0R5Lztz6xufaTYzMC3d
25
+ AZG9XPPjRqSLs+ylSXJpwHEwoeyLFDaJcO+GgW1/ut4MC2HppOx6aImwDdXMHUWR
26
+ KYGIFF4AU/IYoBcanAm4s078EH/Oz01B2c7tR2TqabisPgLYe7PXSCw=
27
+ -----END RSA PRIVATE KEY-----
@@ -0,0 +1,12 @@
1
+ current_dir = File.expand_path(File.dirname(__FILE__))
2
+
3
+ log_level :info
4
+ log_location STDOUT
5
+ node_name "berkshelf"
6
+ client_key "#{current_dir}/berkshelf.pem"
7
+ validation_client_name "chef-validator"
8
+ validation_key "#{current_dir}/chef-validator.pem"
9
+ chef_server_url "http://localhost:4000"
10
+ cache_type 'BasicFile'
11
+ cache_options( :path => "#{ENV['HOME']}/.chef/checksums" )
12
+ cookbook_path []
@@ -22,10 +22,11 @@ Spork.prefork do
22
22
  RSpec.configure do |config|
23
23
  config.include Berkshelf::RSpec::FileSystemMatchers
24
24
  config.include JsonSpec::Helpers
25
+ config.include Berkshelf::RSpec::ChefAPI
25
26
 
26
27
  config.mock_with :rspec
27
28
  config.treat_symbols_as_metadata_keys_with_true_values = true
28
- config.filter_run :focus => true
29
+ config.filter_run focus: true
29
30
  config.run_all_when_everything_filtered = true
30
31
 
31
32
  config.around do |example|
@@ -85,6 +86,8 @@ Spork.prefork do
85
86
  FileUtils.rm_rf(tmp_path)
86
87
  FileUtils.mkdir_p(tmp_path)
87
88
  end
89
+
90
+ Berkshelf::RSpec::Knife.load_knife_config(File.join(APP_ROOT, 'spec/knife.rb'))
88
91
  end
89
92
 
90
93
  Spork.each_run do
@@ -5,6 +5,33 @@ require 'chef/cookbook_version'
5
5
  module Berkshelf
6
6
  module RSpec
7
7
  module ChefAPI
8
+ # Return an array of Hashes containing cookbooks and their information
9
+ #
10
+ # @return [Array]
11
+ def get_cookbooks
12
+ rest.get_rest("cookbooks")
13
+ end
14
+
15
+ def upload_cookbook(path)
16
+ cached = CachedCookbook.from_store_path(path)
17
+ uploader.upload!(cached)
18
+ end
19
+
20
+ # Remove all versions of all cookbooks from the Chef Server defined in your
21
+ # Knife config.
22
+ def purge_cookbooks
23
+ get_cookbooks.each do |name, info|
24
+ info["versions"].each do |version_info|
25
+ rest.delete_rest("cookbooks/#{name}/#{version_info["version"]}?purge=true")
26
+ end
27
+ end
28
+ end
29
+
30
+ # Remove the version of the given cookbook from the Chef Server defined
31
+ # in your Knife config.
32
+ #
33
+ # @param [#to_s] name
34
+ # @param [#to_s] version
8
35
  def purge_cookbook(name, version)
9
36
  rest.delete_rest("cookbooks/#{name}/#{version}?purge=true")
10
37
  rescue Net::HTTPServerException => e
@@ -57,6 +84,7 @@ EOF
57
84
  end
58
85
 
59
86
  File.write(cookbook_path.join("metadata.rb"), metadata)
87
+ cookbook_path
60
88
  end
61
89
 
62
90
  private
@@ -64,6 +92,10 @@ EOF
64
92
  def rest
65
93
  quietly { Chef::REST.new(Chef::Config[:chef_server_url]) }
66
94
  end
95
+
96
+ def uploader
97
+ @uploader ||= Berkshelf::Uploader.new(Chef::Config[:chef_server_url])
98
+ end
67
99
  end
68
100
  end
69
101
  end
@@ -0,0 +1,18 @@
1
+ require 'chef/config'
2
+
3
+ module Berkshelf
4
+ module RSpec
5
+ module Knife
6
+ class << self
7
+ def load_knife_config(path)
8
+ if File.exist?(path)
9
+ Chef::Config.from_file(path)
10
+ ENV["CHEF_CONFIG"] = path
11
+ else
12
+ raise "Cannot continue; '#{path}' must exist and have testing credentials."
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,243 @@
1
+ require 'spec_helper'
2
+
3
+ module Berkshelf
4
+ describe CookbookSource::ChefAPILocation do
5
+ let(:test_chef_api) { "https://chefserver:8081" }
6
+
7
+ describe "ClassMethods" do
8
+ subject { CookbookSource::ChefAPILocation }
9
+ let(:valid_uri) { test_chef_api }
10
+ let(:invalid_uri) { "notauri" }
11
+ let(:constraint) { double('constraint') }
12
+
13
+ describe "::initialize" do
14
+ let(:node_name) { "reset" }
15
+ let(:client_key) { fixtures_path.join("reset.pem").to_s }
16
+
17
+ before(:each) do
18
+ @location = subject.new("nginx",
19
+ constraint,
20
+ chef_api: test_chef_api,
21
+ node_name: node_name,
22
+ client_key: client_key
23
+ )
24
+ end
25
+
26
+ it "sets the uri attribute to the value of the chef_api option" do
27
+ @location.uri.should eql(test_chef_api)
28
+ end
29
+
30
+ it "sets the node_name attribute to the value of the node_name option" do
31
+ @location.node_name.should eql(node_name)
32
+ end
33
+
34
+ it "sets the client_key attribute to the value of the client_key option" do
35
+ @location.client_key.should eql(client_key)
36
+ end
37
+
38
+ it "sets the downloaded status to false" do
39
+ @location.should_not be_downloaded
40
+ end
41
+
42
+ context "when an invalid Chef API URI is given" do
43
+ it "raises InvalidChefAPILocation" do
44
+ lambda {
45
+ subject.new("nginx", constraint, chef_api: invalid_uri, node_name: node_name, client_key: client_key)
46
+ }.should raise_error(InvalidChefAPILocation, "'notauri' is not a valid Chef API URI.")
47
+ end
48
+ end
49
+
50
+ context "when no option for node_name is supplied" do
51
+ it "raises InvalidChefAPILocation" do
52
+ lambda {
53
+ subject.new("nginx", constraint, chef_api: invalid_uri, client_key: client_key)
54
+ }.should raise_error(InvalidChefAPILocation)
55
+ end
56
+ end
57
+
58
+ context "when no option for client_key is supplied" do
59
+ it "raises InvalidChefAPILocation" do
60
+ lambda {
61
+ subject.new("nginx", constraint, chef_api: invalid_uri, node_name: node_name)
62
+ }.should raise_error(InvalidChefAPILocation)
63
+ end
64
+ end
65
+
66
+ context "given the symbol :knife for the value of chef_api:" do
67
+ before(:each) { @loc = subject.new("nginx", constraint, chef_api: :knife) }
68
+
69
+ it "uses the value of Chef::Config[:chef_server_url] for the uri attribute" do
70
+ @loc.uri.should eql(Chef::Config[:chef_server_url])
71
+ end
72
+
73
+ it "uses the value of Chef::Config[:node_name] for the node_name attribute" do
74
+ @loc.node_name.should eql(Chef::Config[:node_name])
75
+ end
76
+
77
+ it "uses the value of Chef::Config[:client_key] for the client_key attribute" do
78
+ @loc.client_key.should eql(Chef::Config[:client_key])
79
+ end
80
+
81
+ it "attempts to load the config file with no arguments" do
82
+ Berkshelf.should_receive(:load_config).with(no_args)
83
+
84
+ subject.new("nginx", constraint, chef_api: :knife)
85
+ end
86
+ end
87
+ end
88
+
89
+ describe "::validate_uri" do
90
+ it "returns false if the given URI is invalid" do
91
+ subject.validate_uri(invalid_uri).should be_false
92
+ end
93
+
94
+ it "returns true if the given URI is valid" do
95
+ subject.validate_uri(valid_uri).should be_true
96
+ end
97
+ end
98
+
99
+ describe "::validate_uri!" do
100
+ it "raises InvalidChefAPILocation if the given URI is invalid" do
101
+ lambda {
102
+ subject.validate_uri!(invalid_uri)
103
+ }.should raise_error(InvalidChefAPILocation, "'notauri' is not a valid Chef API URI.")
104
+ end
105
+
106
+ it "returns true if the given URI is valid" do
107
+ subject.validate_uri!(valid_uri).should be_true
108
+ end
109
+ end
110
+ end
111
+
112
+ subject do
113
+ loc = CookbookSource::ChefAPILocation.new("nginx",
114
+ double('constraint', satisfies?: true),
115
+ chef_api: :knife
116
+ )
117
+ end
118
+
119
+ let(:rest) { double('rest') }
120
+
121
+ before(:each) do
122
+ subject.stub(:rest) { rest }
123
+ end
124
+
125
+ describe "#download" do
126
+ let(:latest) { ["0.101.2", "http://chef/cookbook/nginx/0.101.2"] }
127
+ let(:versions) do
128
+ versions = {
129
+ "#{subject.name}" => {
130
+ "versions" => [
131
+ {
132
+ "url" => "https://api.opscode.com/organizations/vialstudios/cookbooks/#{subject.name}/0.101.2",
133
+ "version" => "0.101.2"
134
+ },
135
+ {
136
+ "url" => "https://api.opscode.com/organizations/vialstudios/cookbooks/#{subject.name}/0.99.0",
137
+ "version" => "0.99.0"
138
+ }
139
+ ],
140
+ "url" => "https://api.opscode.com/organizations/vialstudios/cookbooks/#{subject.name}"
141
+ }
142
+ }
143
+ end
144
+
145
+ before(:each) do
146
+ subject.stub(:latest_version) { latest }
147
+ rest.should_receive(:get_rest).with("cookbooks/#{subject.name}").and_return(versions)
148
+ end
149
+
150
+ context "given a constraint that matches an available cookbook" do
151
+ before(:each) do
152
+ cookbook_version = double('cookbook-version')
153
+ cookbook_version.stub(:manifest).and_return({})
154
+ subject.stub(:version_constraint) { Solve::Constraint.new("= 0.99.0") }
155
+ rest.should_receive(:get_rest).with("https://api.opscode.com/organizations/vialstudios/cookbooks/nginx/0.99.0").and_return(cookbook_version)
156
+ subject.should_receive(:download_files).with(cookbook_version.manifest).and_return(
157
+ generate_cookbook(Dir.mktmpdir, subject.name, "0.99.0")
158
+ )
159
+ end
160
+
161
+ it "returns an instance of Berkshelf::CachedCookbook" do
162
+ subject.download(tmp_path).should be_a(Berkshelf::CachedCookbook)
163
+ end
164
+
165
+ it "sets the downloaded status to true" do
166
+ subject.download(tmp_path)
167
+
168
+ subject.should be_downloaded
169
+ end
170
+ end
171
+
172
+ context "given a wildcard '>= 0.0.0' version constraint is specified" do
173
+ before(:each) do
174
+ subject.stub(:version_constraint) { Solve::Constraint.new(">= 0.0.0") }
175
+ end
176
+
177
+ it "downloads the manifest of the latest cookbook version of the cookbook" do
178
+ cookbook_version = double('cookbook-version')
179
+ cookbook_version.stub(:manifest).and_return({})
180
+ rest.should_receive(:get_rest).with("https://api.opscode.com/organizations/vialstudios/cookbooks/nginx/0.101.2").and_return(cookbook_version)
181
+ subject.should_receive(:download_files).with(cookbook_version.manifest).and_return(
182
+ generate_cookbook(Dir.mktmpdir, subject.name, subject.latest_version[0])
183
+ )
184
+
185
+ subject.download(tmp_path)
186
+ end
187
+ end
188
+ end
189
+
190
+ describe "#versions" do
191
+ before(:each) do
192
+ rest = double('rest')
193
+ subject.stub(:rest) { rest }
194
+ response = {"nginx"=>{"versions"=>[{"url"=>"https://api.opscode.com/organizations/vialstudios/cookbooks/nginx/0.101.2", "version"=>"0.101.2"}], "url"=>"https://api.opscode.com/organizations/vialstudios/cookbooks/nginx"}}
195
+ rest.stub(:get_rest).and_return(response)
196
+ end
197
+
198
+ it "returns a hash containing a string containing the version number of each cookbook version as the keys" do
199
+ subject.versions.should have_key("0.101.2")
200
+ end
201
+
202
+ it "returns a hash containing a string containing the download URL for each cookbook version as the values" do
203
+ subject.versions["0.101.2"].should eql("https://api.opscode.com/organizations/vialstudios/cookbooks/nginx/0.101.2")
204
+ end
205
+ end
206
+
207
+ describe "#latest_version" do
208
+ before(:each) do
209
+ subject.stub(:versions).and_return(
210
+ "0.0.1" => "https://chef/nginx/0.0.1",
211
+ "1.0.0" => "https://chef/nginx/1.0.0",
212
+ "0.100.0" => "https://chef/nginx/0.100.0"
213
+ )
214
+ end
215
+
216
+ it "returns an array with two elements" do
217
+ subject.latest_version.should be_a(Array)
218
+ subject.latest_version.should have(2).items
219
+ end
220
+
221
+ it "returns an array containing the latest version at index 0" do
222
+ subject.latest_version[0].should eql("1.0.0")
223
+ end
224
+
225
+ it "returns an array containing the URL to the latest version at index 1" do
226
+ subject.latest_version[1].should eql("https://chef/nginx/1.0.0")
227
+ end
228
+ end
229
+
230
+ describe "#to_s" do
231
+ subject do
232
+ CookbookSource::ChefAPILocation.new('nginx',
233
+ double('constraint'),
234
+ chef_api: :knife
235
+ )
236
+ end
237
+
238
+ it "returns a string containing the location key and the Chef API URI" do
239
+ subject.to_s.should eql("chef_api: '#{Chef::Config[:chef_server_url]}'")
240
+ end
241
+ end
242
+ end
243
+ end