berkshelf 0.3.7 → 0.4.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
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