Skip to content
This repository was archived by the owner on Jun 2, 2021. It is now read-only.

Commit a637d95

Browse files
committed
Raise new UniquenessError on duplicate app names
We recently did some explorations into how we can better use v3 API error `title`s + `code`s to allow clients to distinguish between 422s, which are returned in many cases, without needing to resort to the `detail` field. The result was that we wanted to add new categories of 422 errors, identified by their `title`/`code`. These errors will still have a `detail` field that contains their original, longer message. The first category that we're introducing is the `UniquenessError` (code `10016`). We now raise it when trying to create an app with a name that already exists in the target space. Previously, this had title `UnprocessableEntity` (code `10008`), which did not give any more useful information than the 422 HTTP status code already gave. The error `detail` is unchanged. This commit also introduces some patterns and helpers that will hopefully be useful as we do more of this kind of thing: we have `api_error!` and `v3_api_error!` methods that can raise new `ApiError`s and `V3::ApiErrors`, respectively. And we have corresponding `raise_api_error` and `raise_v3_api_error` RSpec matchers to make it easier to assert that we raised the right error. [finishes #173506370] Authored-by: Reid Mitchell <rmitchell@pivotal.io>
1 parent c99f82e commit a637d95

5 files changed

Lines changed: 100 additions & 14 deletions

File tree

app/actions/app_create.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
require 'process_create'
22
require 'models/helpers/process_types'
33
require 'actions/labels_update'
4+
require 'cloud_controller/errors/api_error_helpers'
45

56
module VCAP::CloudController
67
class AppCreate
8+
include CloudController::Errors::ApiErrorHelpers
9+
710
class InvalidApp < StandardError; end
811

912
def initialize(user_audit_info)
@@ -25,7 +28,7 @@ def create(message, lifecycle)
2528

2629
MetadataUpdate.update(app, message)
2730

28-
raise CloudController::Errors::ApiError.new_from_details('CustomBuildpacksDisabled') if using_disabled_custom_buildpack?(app)
31+
api_error!(:CustomBuildpacksDisabled) if using_disabled_custom_buildpack?(app)
2932

3033
ProcessCreate.new(@user_audit_info).create(app, {
3134
guid: app.guid,
@@ -42,6 +45,10 @@ def create(message, lifecycle)
4245

4346
app
4447
rescue Sequel::ValidationFailed => e
48+
if e.errors.on([:space_guid, :name])
49+
v3_api_error!(:UniquenessError, e.message)
50+
end
51+
4552
raise InvalidApp.new(e.message)
4653
end
4754

errors/v3.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
10016:
2+
name: UniquenessError
3+
http_code: 422
4+
message: "%s"
5+
16
270010:
27
name: ServiceBrokerNotRemovable
38
http_code: 422
@@ -6,4 +11,4 @@
611
270011:
712
name: ServiceBrokerGone
813
http_code: 422
9-
message: "The service broker was removed before the synchronization completed"
14+
message: "The service broker was removed before the synchronization completed"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module CloudController
2+
module Errors
3+
module ApiErrorHelpers
4+
def api_error!(name, *args)
5+
raise CloudController::Errors::ApiError.new_from_details(name, *args)
6+
end
7+
8+
def v3_api_error!(name, *args)
9+
raise CloudController::Errors::V3::ApiError.new_from_details(name, *args)
10+
end
11+
end
12+
end
13+
end
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Allows making assertions like:
2+
# .to raise_api_error(:UniquenessError, "some-message")
3+
# or like:
4+
# .to raise_api_error(:UniquenessError)
5+
# If only the error name is given, the additional arguments will not be considered
6+
# when matching API errors. This allows slightly more relaxed assertions.
7+
RSpec::Matchers.define :raise_api_error do |api_error_name, *api_error_args|
8+
build_api_error_matcher(CloudController::Errors::ApiError, api_error_name, *api_error_args)
9+
end
10+
11+
# Similar to the above, but looks for a Errors::V3::ApiError instead of Errors::ApiError
12+
RSpec::Matchers.define :raise_v3_api_error do |api_error_name, *api_error_args|
13+
build_api_error_matcher(CloudController::Errors::V3::ApiError, api_error_name, *api_error_args)
14+
end
15+
16+
def build_api_error_matcher(error_class, api_error_name, *api_error_args)
17+
supports_block_expectations
18+
19+
match do |actual|
20+
actual.call
21+
rescue error_class => actual_api_error
22+
@actual_api_error = actual_api_error
23+
expected_error = error_class.new_from_details(api_error_name, *api_error_args)
24+
25+
if api_error_args.empty?
26+
expected_error.name == actual_api_error.name
27+
else
28+
expected_error == actual_api_error
29+
end
30+
rescue => other_error
31+
@other_error = other_error
32+
end
33+
34+
failure_message do
35+
message = "expected block to raise a #{error_class} with:\n" \
36+
" name: #{api_error_name}\n" \
37+
" args: #{api_error_args}\nbut "
38+
39+
message += if @actual_api_error
40+
"it raised a #{error_class} with:\n" \
41+
" name: #{@actual_api_error.name}\n" \
42+
" args: #{@actual_api_error.args}"
43+
elsif @other_error
44+
"it raised #{@other_error}"
45+
else
46+
'it did not raise'
47+
end
48+
49+
message
50+
end
51+
end

spec/unit/actions/app_create_spec.rb

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,19 @@ module VCAP::CloudController
2020
let(:message) do
2121
AppCreateMessage.new(
2222
{
23-
name: 'my-app',
24-
relationships: relationships,
23+
name: 'my-app',
24+
relationships: relationships,
2525
environment_variables: environment_variables,
26-
lifecycle: lifecycle_request,
26+
lifecycle: lifecycle_request,
2727
metadata: {
28-
labels: {
29-
release: 'stable',
30-
'seriouseats.com/potato': 'mashed'
31-
},
32-
annotations: {
33-
superhero: 'Bummer-boy',
34-
superpower: 'Bums you out',
35-
}
28+
labels: {
29+
release: 'stable',
30+
'seriouseats.com/potato': 'mashed'
31+
},
32+
annotations: {
33+
superhero: 'Bummer-boy',
34+
superpower: 'Bums you out',
35+
}
3636
}
3737
})
3838
end
@@ -141,7 +141,7 @@ module VCAP::CloudController
141141
it 'raises an error' do
142142
expect {
143143
app_create.create(message, lifecycle)
144-
}.to raise_error(CloudController::Errors::ApiError, /Custom buildpacks are disabled/)
144+
}.to raise_api_error(:CustomBuildpacksDisabled)
145145
end
146146

147147
it 'does not create an app' do
@@ -168,6 +168,16 @@ module VCAP::CloudController
168168
app_create.create(message, lifecycle)
169169
}.to raise_error(AppCreate::InvalidApp)
170170
end
171+
172+
context 'when the app name is a duplicate within the space' do
173+
let!(:existing_app) { AppModel.make(space: space, name: 'my-app') }
174+
175+
it 'fails the right way' do
176+
expect {
177+
app_create.create(message, lifecycle)
178+
}.to raise_v3_api_error(:UniquenessError)
179+
end
180+
end
171181
end
172182
end
173183
end

0 commit comments

Comments
 (0)