engineyard 1.2.1 → 1.2.2

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.
@@ -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