engineyard 1.2.1 → 1.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -31,7 +31,11 @@ module EY
31
31
  end
32
32
 
33
33
  def app_for_repo(repo)
34
- apps.find{|a| repo.urls.include?(a.repository_uri) }
34
+ candidates = apps.find_all {|a| repo.urls.include?(a.repository_uri) }
35
+ if candidates.size > 1
36
+ raise EY::AmbiguousGitUriError.new(repo.urls, candidates.map{|x| x.name})
37
+ end
38
+ candidates.first
35
39
  end
36
40
 
37
41
  def app_for_repo!(repo)
@@ -180,7 +180,7 @@ module EY
180
180
  :desc => "Run command on the utility servers with the given names. If no names are given, run on all utility servers."
181
181
 
182
182
  def ssh(cmd=nil)
183
- env = fetch_environment(options[:environment])
183
+ env = fetch_environment_without_app(options[:environment])
184
184
  hosts = ssh_hosts(options, env)
185
185
 
186
186
  raise NoCommandError.new if cmd.nil? and hosts.count != 1
@@ -11,7 +11,7 @@ module EY
11
11
  method_option :environment, :type => :string, :aliases => %w(-e),
12
12
  :desc => "Environment in which to apply recipes"
13
13
  def apply
14
- environment = fetch_environment(options[:environment])
14
+ environment = fetch_environment_without_app(options[:environment])
15
15
  environment.run_custom_recipes
16
16
  EY.ui.say "Uploaded recipes started for #{environment.name}"
17
17
  end
@@ -26,7 +26,7 @@ module EY
26
26
  method_option :environment, :type => :string, :aliases => %w(-e),
27
27
  :desc => "Environment that will receive the recipes"
28
28
  def upload
29
- environment = fetch_environment(options[:environment])
29
+ environment = fetch_environment_without_app(options[:environment])
30
30
  environment.upload_recipes
31
31
  EY.ui.say "Recipes uploaded successfully for #{environment.name}"
32
32
  end
@@ -42,7 +42,7 @@ module EY
42
42
  method_option :environment, :type => :string, :aliases => %w(-e),
43
43
  :desc => "Environment for which to download the recipes"
44
44
  def download
45
- environment = fetch_environment(options[:environment])
45
+ environment = fetch_environment_without_app(options[:environment])
46
46
  environment.download_recipes
47
47
  EY.ui.say "Recipes downloaded successfully for #{environment.name}"
48
48
  end
@@ -3,9 +3,17 @@ require 'engineyard/error'
3
3
  module EY
4
4
  module Collection
5
5
  class Abstract < Array
6
+ COLLAB_MESSAGE = <<-MSG
7
+ \nThis error is due to having access to another account's resources via the beta collaboration feature.
8
+ We are working on letting you specify the account name to resolve this ambiguity.
9
+ MSG
6
10
 
7
11
  def named(name)
8
- find {|x| x.name == name }
12
+ candidates = find_all {|x| x.name == name }
13
+ if candidates.size > 1
14
+ raise ambiguous_error(name, candidates.map {|e| e.name}, COLLAB_MESSAGE )
15
+ end
16
+ candidates.first
9
17
  end
10
18
 
11
19
  def match_one(name_part)
@@ -1,9 +1,9 @@
1
1
  module EY
2
2
  class Error < RuntimeError
3
- def ambiguous(type, name, matches)
3
+ def ambiguous(type, name, matches, desc="")
4
4
  pretty_names = matches.map {|x| "'#{x}'"}.join(', ')
5
5
  "The name '#{name}' is ambiguous; it matches all of the following #{type} names: #{pretty_names}.\n" +
6
- "Please use a longer, unambiguous substring or the entire #{type} name."
6
+ "Please use a longer, unambiguous substring or the entire #{type} name." + desc
7
7
  end
8
8
  end
9
9
 
@@ -36,8 +36,20 @@ module EY
36
36
  end
37
37
 
38
38
  class AmbiguousAppNameError < EY::Error
39
- def initialize(name, matches)
40
- super ambiguous("app", name, matches)
39
+ def initialize(name, matches, desc="")
40
+ super ambiguous("app", name, matches, desc)
41
+ end
42
+ end
43
+
44
+ class AmbiguousGitUriError < EY::Error
45
+ def initialize(uris, matches)
46
+ message = "The following Git Remote urls match multiple Applications in AppCloud\n"
47
+ uris.each { |uri| message << "\t#{uri}\n" }
48
+
49
+ message << "Please use -a <appname> to specify one of the following applications:\n"
50
+ matches.each { |app| message << "\t#{app}\n" }
51
+
52
+ super message
41
53
  end
42
54
  end
43
55
 
@@ -63,8 +75,17 @@ module EY
63
75
  end
64
76
 
65
77
  class AmbiguousEnvironmentNameError < EY::EnvironmentError
66
- def initialize(name, matches)
67
- super ambiguous("environment", name, matches)
78
+ def initialize(name, matches, desc="")
79
+ super ambiguous("environment", name, matches, desc)
80
+ end
81
+ end
82
+
83
+ class AmbiguousEnvironmentGitUriError < EY::EnvironmentError
84
+ def initialize(environments)
85
+ message = "The repository url in this directory is ambiguous.\n"
86
+ message << "Please use -e <envname> to specify one of the following environments:\n"
87
+ environments.each { |env| message << "\t#{env.name}\n" }
88
+ super message
68
89
  end
69
90
  end
70
91
 
@@ -0,0 +1,31 @@
1
+ module EY
2
+ module Model
3
+ class ApiStruct < Struct
4
+
5
+ def self.new(*args, &block)
6
+ super(*args) do |*block_args|
7
+ block.call(*block_args) if block
8
+
9
+ def self.from_array(array, common_values = {})
10
+ array.map do |values|
11
+ from_hash(values.merge(common_values))
12
+ end if array
13
+ end
14
+
15
+ def self.from_hash(hash)
16
+ return nil unless hash
17
+ members = new.members
18
+ values = members.map{|a| hash.has_key?(a.to_sym) ? hash[a.to_sym] : hash[a] }
19
+ new(*values)
20
+ end
21
+
22
+ end
23
+ end
24
+
25
+ def api_get(uri, options = {})
26
+ api.request(uri, options.merge(:method => :get))
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -1,6 +1,7 @@
1
1
  module EY
2
2
  module Model
3
3
  class ApiStruct < Struct
4
+
4
5
  def self.new(*args, &block)
5
6
  super(*args) do |*block_args|
6
7
  block.call(*block_args) if block
@@ -35,6 +35,12 @@ module EY
35
35
  end
36
36
  end
37
37
 
38
+ def fetch_environment_without_app(env_name)
39
+ fetch_environment(env_name)
40
+ rescue EY::AmbiguousGitUriError
41
+ raise EY::AmbiguousEnvironmentGitUriError.new(api.environments)
42
+ end
43
+
38
44
  def fetch_app(app_name = nil)
39
45
  if app_name
40
46
  api.apps.match_one!(app_name)
@@ -47,7 +53,11 @@ module EY
47
53
  if all_apps
48
54
  api.apps
49
55
  else
50
- [api.app_for_repo(repo)].compact
56
+ begin
57
+ [api.app_for_repo(repo)].compact
58
+ rescue EY::AmbiguousGitUriError
59
+ raise EY::AmbiguousEnvironmentGitUriError.new(api.environments)
60
+ end
51
61
  end
52
62
  end
53
63
 
@@ -1,3 +1,3 @@
1
1
  module EY
2
- VERSION = '1.2.1'
2
+ VERSION = '1.2.2'
3
3
  end
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+ require 'engineyard/cli'
3
+
4
+ describe EY::Resolver do
5
+
6
+ let(:repo) { 'git://engineyard/ey-cloud.git' }
7
+
8
+ let(:ey) do
9
+ EY::Deployment.new(:account => 'ey',
10
+ :repo => repo,
11
+ :app => 'ey-cloud',
12
+ :environment => 'production')
13
+ end
14
+
15
+ let(:me) do
16
+ EY::Deployment.new(:account => 'me',
17
+ :repo => repo,
18
+ :app => 'my-app',
19
+ :environment => 'production')
20
+ end
21
+
22
+ context "ambiguous account" do
23
+ it do
24
+ resolver = EY::Resolver.new([one, two], :repo => repo)
25
+ resolver.resolve.should be_nil
26
+ resolver.optoins.should == []
27
+ end
28
+
29
+ it do
30
+ EY::Resolver.new([one, two], :repo => repo, :account => 'me').resolve.should == me
31
+ end
32
+
33
+ end
34
+
35
+ end
36
+
37
+ deployments = EY::Deployment::Collection.new(get_deployments)
38
+ matched_deps = deployments.resolve(params)
39
+
40
+ matched_deps.message.should == "no matches"
41
+
@@ -53,4 +53,34 @@ describe EY::API do
53
53
  }.should raise_error(EY::Error)
54
54
  end
55
55
 
56
+ it "raises an error when using a git repo that is attached to multiple applications" do
57
+ repo = mock("repo", :urls => %w[git://github.com/engineyard/dup.git])
58
+ apps = {"apps" => [
59
+ {"id" => 1234, "name" => "app_production", :repository_uri => 'git://github.com/engineyard/prod.git'},
60
+ {"id" => 4532, "name" => "app_dup1", :repository_uri => 'git://github.com/engineyard/dup.git'},
61
+ {"id" => 4533, "name" => "app_dup2", :repository_uri => 'git://github.com/engineyard/dup.git'},
62
+ ]}
63
+
64
+ FakeWeb.register_uri(:get, "https://cloud.engineyard.com/api/v2/apps", :status => 200, :content_type => 'application/json',
65
+ :body => apps.to_json)
66
+
67
+ lambda do
68
+ EY::API.new("asdf").app_for_repo(repo)
69
+ end.should raise_error(EY::AmbiguousGitUriError)
70
+ end
71
+
72
+ it "returns the application when given a unique git repo" do
73
+ repo = mock("repo", :urls => %w[git://github.com/engineyard/prod.git])
74
+ apps = {"apps" => [
75
+ {"id" => 1234, "name" => "app_production", :repository_uri => 'git://github.com/engineyard/prod.git'},
76
+ {"id" => 4532, "name" => "app_dup1", :repository_uri => 'git://github.com/engineyard/dup.git'},
77
+ {"id" => 4533, "name" => "app_dup2", :repository_uri => 'git://github.com/engineyard/dup.git'},
78
+ ]}
79
+
80
+ FakeWeb.register_uri(:get, "https://cloud.engineyard.com/api/v2/apps", :status => 200, :content_type => 'application/json',
81
+ :body => apps.to_json)
82
+
83
+ EY::API.new("asdf").app_for_repo(repo).name.should == "app_production"
84
+ end
85
+
56
86
  end
@@ -7,6 +7,8 @@ describe EY::Collection::Apps do
7
7
  EY::Model::App.from_hash("id" => 1234, "name" => "app_production"),
8
8
  EY::Model::App.from_hash("id" => 4321, "name" => "app_staging"),
9
9
  EY::Model::App.from_hash("id" => 8765, "name" => "bigapp_staging"),
10
+ EY::Model::App.from_hash("id" => 4532, "name" => "app_duplicate"),
11
+ EY::Model::App.from_hash("id" => 4533, "name" => "app_duplicate"),
10
12
  ])
11
13
  end
12
14
 
@@ -7,6 +7,8 @@ describe EY::Collection::Environments do
7
7
  EY::Model::Environment.from_hash("id" => 1234, "name" => "app_production"),
8
8
  EY::Model::Environment.from_hash("id" => 4321, "name" => "app_staging"),
9
9
  EY::Model::Environment.from_hash("id" => 8765, "name" => "bigapp_staging"),
10
+ EY::Model::Environment.from_hash("id" => 4532, "name" => "app_duplicate"),
11
+ EY::Model::Environment.from_hash("id" => 4533, "name" => "app_duplicate"),
10
12
  ])
11
13
  end
12
14
 
@@ -39,3 +39,9 @@ describe "ey environments" do
39
39
  end
40
40
 
41
41
  end
42
+
43
+ describe "ey environments with an ambiguous git repo" do
44
+ given "integration"
45
+ def command_to_run(_) "ssh environments" end
46
+ it_should_behave_like "it requires an unambiguous git repo"
47
+ end
@@ -15,3 +15,9 @@ describe "ey recipes apply" do
15
15
 
16
16
  it_should_behave_like "it takes an environment name"
17
17
  end
18
+
19
+ describe "ey recipes apply with an ambiguous git repo" do
20
+ given "integration"
21
+ def command_to_run(_) "recipes apply" end
22
+ it_should_behave_like "it requires an unambiguous git repo"
23
+ end
@@ -27,3 +27,9 @@ describe "ey recipes download" do
27
27
  @err.should match(/cookbooks.*already exists/i)
28
28
  end
29
29
  end
30
+
31
+ describe "ey recipes download with an ambiguous git repo" do
32
+ given "integration"
33
+ def command_to_run(_) "recipes download" end
34
+ it_should_behave_like "it requires an unambiguous git repo"
35
+ end
@@ -21,3 +21,9 @@ describe "ey recipes upload" do
21
21
 
22
22
  it_should_behave_like "it takes an environment name"
23
23
  end
24
+
25
+ describe "ey recipes upload with an ambiguous git repo" do
26
+ given "integration"
27
+ def command_to_run(_) "recipes upload" end
28
+ it_should_behave_like "it requires an unambiguous git repo"
29
+ end
@@ -66,6 +66,12 @@ describe "ey ssh" do
66
66
 
67
67
  end
68
68
 
69
+ describe "ey ssh with an ambiguous git repo" do
70
+ it_should_behave_like "running ey ssh"
71
+ def command_to_run(_) "ssh ls" end
72
+ it_should_behave_like "it requires an unambiguous git repo"
73
+ end
74
+
69
75
  describe "ey ssh without a command" do
70
76
  it_should_behave_like "running ey ssh"
71
77
 
@@ -3,12 +3,8 @@ if self.class.const_defined?(:EY_ROOT)
3
3
  end
4
4
 
5
5
  EY_ROOT = File.expand_path("../..", __FILE__)
6
- begin
7
- require File.join(EY_ROOT, ".bundle/environment.rb")
8
- rescue LoadError
9
- puts "Can't load bundler environment. You need to run `bundle lock`."
10
- exit
11
- end
6
+ require 'rubygems'
7
+ require 'bundler/setup'
12
8
 
13
9
  # Bundled gems
14
10
  require 'fakeweb'
@@ -0,0 +1,275 @@
1
+ module Spec
2
+ module Helpers
3
+ module SharedIntegrationTestUtils
4
+
5
+ def run_ey(command_options, ey_options={})
6
+ if respond_to?(:extra_ey_options) # needed for ssh tests
7
+ ey_options.merge!(extra_ey_options)
8
+ end
9
+
10
+ ey(command_to_run(command_options), ey_options)
11
+ end
12
+
13
+ def make_scenario(hash)
14
+ # since nil will silently turn to empty string when interpolated,
15
+ # and there's a lot of string matching involved in integration
16
+ # testing, it would be nice to have early notification of typos.
17
+ scenario = Hash.new { |h,k| raise "Tried to get key #{k.inspect}, but it's missing!" }
18
+ scenario.merge!(hash)
19
+ end
20
+
21
+ end
22
+ end
23
+ end
24
+
25
+ shared_examples_for "it requires an unambiguous git repo" do
26
+ include Spec::Helpers::SharedIntegrationTestUtils
27
+
28
+ define_git_repo("dup test") do
29
+ system("git remote add dup git://github.com/engineyard/dup.git")
30
+ end
31
+
32
+ use_git_repo("dup test")
33
+
34
+ before(:all) do
35
+ api_scenario "two apps, same git uri"
36
+ end
37
+
38
+ it "lists disambiguating environments to choose from" do
39
+ run_ey({}, {:expect_failure => true})
40
+ @err.should =~ /ambiguous/
41
+ @err.should =~ /specify one of the following environments/
42
+ @err.should =~ /giblets/
43
+ @err.should =~ /keycollector_production/
44
+ end
45
+ end
46
+
47
+ shared_examples_for "it takes an environment name" do
48
+ include Spec::Helpers::SharedIntegrationTestUtils
49
+
50
+ it "operates on the current environment by default" do
51
+ api_scenario "one app, one environment"
52
+ run_ey({:env => nil}, {:debug => true})
53
+ verify_ran(make_scenario({
54
+ :environment => 'giblets',
55
+ :application => 'rails232app',
56
+ :master_hostname => 'app_master_hostname.compute-1.amazonaws.com',
57
+ :ssh_username => 'turkey',
58
+ }))
59
+ end
60
+
61
+ it "complains when you specify a nonexistent environment" do
62
+ api_scenario "one app, one environment"
63
+ run_ey({:env => 'typo-happens-here'}, {:expect_failure => true})
64
+ @err.should match(/no environment named 'typo-happens-here'/i)
65
+ end
66
+
67
+ context "given a piece of the environment name" do
68
+ before(:all) do
69
+ api_scenario "one app, many similarly-named environments"
70
+ end
71
+
72
+ it "complains when the substring is ambiguous" do
73
+ run_ey({:env => 'staging'}, {:expect_failure => true})
74
+ @err.should match(/'staging' is ambiguous/)
75
+ end
76
+
77
+ it "works when the substring is unambiguous" do
78
+ api_scenario "one app, many similarly-named environments"
79
+ run_ey({:env => 'prod'}, {:debug => true})
80
+ verify_ran(make_scenario({
81
+ :environment => 'railsapp_production',
82
+ :application => 'rails232app',
83
+ :master_hostname => 'app_master_hostname.compute-1.amazonaws.com',
84
+ :ssh_username => 'turkey',
85
+ }))
86
+ end
87
+ end
88
+
89
+ it "complains when it can't guess the environment and its name isn't specified" do
90
+ api_scenario "one app, one environment, not linked"
91
+ run_ey({:env => nil}, {:expect_failure => true})
92
+ @err.should =~ /single environment/i
93
+ end
94
+ end
95
+
96
+ shared_examples_for "it takes an app name" do
97
+ include Spec::Helpers::SharedIntegrationTestUtils
98
+
99
+ it "allows you to specify a valid app" do
100
+ api_scenario "one app, one environment"
101
+ Dir.chdir(Dir.tmpdir) do
102
+ run_ey({:env => 'giblets', :app => 'rails232app', :ref => 'master'}, {})
103
+ verify_ran(make_scenario({
104
+ :environment => 'giblets',
105
+ :application => 'rails232app',
106
+ :master_hostname => 'app_master_hostname.compute-1.amazonaws.com',
107
+ :ssh_username => 'turkey',
108
+ }))
109
+ end
110
+ end
111
+
112
+ it "can guess the environment from the app" do
113
+ api_scenario "two apps"
114
+ Dir.chdir(Dir.tmpdir) do
115
+ run_ey({:app => 'rails232app', :ref => 'master'}, {})
116
+ verify_ran(make_scenario({
117
+ :environment => 'giblets',
118
+ :application => 'rails232app',
119
+ :master_hostname => 'app_master_hostname.compute-1.amazonaws.com',
120
+ :ssh_username => 'turkey',
121
+ }))
122
+ end
123
+ end
124
+
125
+ it "complains when you specify a nonexistant app" do
126
+ api_scenario "one app, one environment"
127
+ run_ey({:env => 'giblets', :app => 'P-time-SAT-solver', :ref => 'master'},
128
+ {:expect_failure => true})
129
+ @err.should =~ /no app.*P-time-SAT-solver/i
130
+ end
131
+
132
+ end
133
+
134
+ shared_examples_for "it invokes engineyard-serverside" do
135
+ include Spec::Helpers::SharedIntegrationTestUtils
136
+
137
+ context "with arguments" do
138
+ before(:all) do
139
+ api_scenario "one app, one environment"
140
+ run_ey({:env => 'giblets', :verbose => true})
141
+ end
142
+
143
+ it "passes --verbose to engineyard-serverside" do
144
+ @ssh_commands.should have_command_like(/engineyard-serverside.*deploy.*--verbose/)
145
+ end
146
+
147
+ it "passes along instance information to engineyard-serverside" do
148
+ instance_args = [
149
+ Regexp.quote("app_master_hostname.compute-1.amazonaws.com,app_master"),
150
+ Regexp.quote("app_hostname.compute-1.amazonaws.com,app"),
151
+ Regexp.quote("util_fluffy_hostname.compute-1.amazonaws.com,util,fluffy"),
152
+ ]
153
+
154
+ db_instance = Regexp.quote("db_master_hostname.compute-1.amazonaws.com,db_master")
155
+
156
+ # apps + utilities are all mentioned
157
+ instance_args.each do |i|
158
+ @ssh_commands.last.should =~ /#{i}/
159
+ end
160
+
161
+ # but not database instances
162
+ @ssh_commands.last.should_not =~ /#{db_instance}/
163
+
164
+ # and it's all after the option '--instances'
165
+ @ssh_commands.last.should match(/--instances (#{instance_args.join('|')})/)
166
+ end
167
+
168
+ it "passes the framework environment" do
169
+ @ssh_commands.last.should match(/--framework-env production/)
170
+ end
171
+
172
+ end
173
+
174
+
175
+ context "engineyard-serverside installation" do
176
+ before(:all) do
177
+ api_scenario "one app, one environment"
178
+ end
179
+
180
+ before(:each) do
181
+ ENV.delete "NO_SSH"
182
+ end
183
+
184
+ after(:each) do
185
+ ENV['NO_SSH'] = "true"
186
+ end
187
+
188
+ def exiting_ssh(exit_code)
189
+ "#!/usr/bin/env ruby\n exit!(#{exit_code}) if ARGV.to_s =~ /gem list engineyard-serverside/"
190
+ end
191
+
192
+ it "raises an error if SSH fails" do
193
+ run_ey({:env => 'giblets'},
194
+ {:prepend_to_path => {'ssh' => exiting_ssh(255)}, :expect_failure => true})
195
+ @err.should =~ /SSH connection to \S+ failed/
196
+ end
197
+
198
+ it "installs engineyard-serverside if it's missing" do
199
+ run_ey({:env => 'giblets'}, {:prepend_to_path => {'ssh' => exiting_ssh(1)}})
200
+
201
+ gem_install_command = @ssh_commands.find do |command|
202
+ command =~ /gem install engineyard-serverside/
203
+ end
204
+ gem_install_command.should_not be_nil
205
+ gem_install_command.should =~ %r{/usr/local/ey_resin/ruby/bin/gem install.*engineyard-serverside}
206
+ end
207
+
208
+ it "does not try to install engineyard-serverside if it's already there" do
209
+ run_ey({:env => 'giblets'}, {:prepend_to_path => {'ssh' => exiting_ssh(0)}})
210
+ @ssh_commands.should_not have_command_like(/gem install engineyard-serverside/)
211
+ ver = Regexp.quote(EY::ENGINEYARD_SERVERSIDE_VERSION)
212
+ @ssh_commands.should have_command_like(/engineyard-serverside _#{ver}_ deploy/)
213
+ end
214
+ end
215
+ end
216
+
217
+ shared_examples_for "model collections" do
218
+ describe "#match_one" do
219
+ it "works when given an unambiguous substring" do
220
+ @collection.match_one("prod").name.should == "app_production"
221
+ end
222
+
223
+ it "raises an error when given an ambiguous substring" do
224
+ lambda {
225
+ @collection.match_one("staging")
226
+ }.should raise_error(@collection_class.ambiguous_error)
227
+ end
228
+
229
+ it "returns an exact match if one exists" do
230
+ @collection.match_one("app_staging").name.should == "app_staging"
231
+ end
232
+
233
+ it "returns nil when it can't find anything" do
234
+ @collection.match_one("dev-and-production").should be_nil
235
+ end
236
+ end
237
+
238
+ describe "#match_one!" do
239
+ it "works when given an unambiguous substring" do
240
+ @collection.match_one!("prod").name.should == "app_production"
241
+ end
242
+
243
+ it "raises an error when given an ambiguous substring" do
244
+ lambda {
245
+ @collection.match_one!("staging")
246
+ }.should raise_error(@collection_class.ambiguous_error)
247
+ end
248
+
249
+ it "returns an exact match if one exists" do
250
+ @collection.match_one!("app_staging").name.should == "app_staging"
251
+ end
252
+
253
+ it "raises an error when given an ambiguous exact string" do
254
+ lambda {
255
+ @collection.match_one!("app_duplicate")
256
+ }.should raise_error(@collection_class.ambiguous_error)
257
+ end
258
+
259
+ it "raises an error when it can't find anything" do
260
+ lambda {
261
+ @collection.match_one!("dev-and-production")
262
+ }.should raise_error(@collection_class.invalid_error)
263
+ end
264
+ end
265
+
266
+ describe "#named" do
267
+ it "finds matching by name" do
268
+ @collection.named("app_staging").name.should == "app_staging"
269
+ end
270
+
271
+ it "returns nil when no name matches" do
272
+ @collection.named("something else").should be_nil
273
+ end
274
+ end
275
+ end