Skip to content

Commit fb900cd

Browse files
reidmitluan
andcommitted
Add /v3/routes endpoint
- Allow for paginated listing of routes, filtered by page and per_page [Finished #162402733] Co-authored-by: Luan Santos <[email protected]> Co-authored-by: Reid Mitchell <[email protected]>
1 parent 6002632 commit fb900cd

File tree

9 files changed

+251
-34
lines changed

9 files changed

+251
-34
lines changed

Diff for: app/controllers/v3/routes_controller.rb

+16
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,25 @@
11
require 'messages/route_create_message'
22
require 'messages/route_show_message'
3+
require 'messages/routes_list_message'
34
require 'presenters/v3/route_presenter'
5+
require 'presenters/v3/paginated_list_presenter'
46
require 'actions/route_create'
57

68
class RoutesController < ApplicationController
9+
def index
10+
message = RoutesListMessage.from_params(query_params)
11+
invalid_param!(message.errors.full_messages) unless message.valid?
12+
13+
dataset = Route.where(guid: permission_queryer.readable_route_guids)
14+
15+
render status: :ok, json: Presenters::V3::PaginatedListPresenter.new(
16+
presenter: Presenters::V3::RoutePresenter,
17+
paginated_result: SequelPaginator.new.get_page(dataset, message.try(:pagination_options)),
18+
path: '/v3/routes',
19+
message: message,
20+
)
21+
end
22+
723
def show
824
message = RouteShowMessage.new({ guid: hashed_params['guid'] })
925
unprocessable!(message.errors.full_messages) unless message.valid?

Diff for: app/messages/routes_list_message.rb

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
require 'messages/metadata_list_message'
2+
3+
module VCAP::CloudController
4+
class RoutesListMessage < ListMessage
5+
def self.from_params(params)
6+
super(params, [])
7+
end
8+
end
9+
end

Diff for: config/routes.rb

+1
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@
146146
get '/apps/:app_guid/route_mappings', to: 'route_mappings#index'
147147

148148
# routes
149+
get '/routes', to: 'routes#index'
149150
post '/routes', to: 'routes#create'
150151
get '/routes/:guid', to: 'routes#show'
151152

Diff for: docs/v3/source/includes/api_resources/_routes.erb

+76-26
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,83 @@
1-
<% content_for :single_route do | metadata={} | %>
1+
<% content_for :single_route do %>
22
{
3-
"guid": "cbad697f-cac1-48f4-9017-ac08f39dfb31",
4-
"host": "a-hostname",
5-
"path": "/some_path",
6-
"created_at": "2019-05-10T17:17:48Z",
7-
"updated_at": "2019-05-10T17:17:48Z",
8-
"relationships": {
9-
"space": {
10-
"data": {
3+
"guid": "cbad697f-cac1-48f4-9017-ac08f39dfb31",
4+
"host": "a-hostname",
5+
"path": "/some_path",
6+
"created_at": "2019-05-10T17:17:48Z",
7+
"updated_at": "2019-05-10T17:17:48Z",
8+
"relationships": {
9+
"space": {
10+
"data": {
11+
"guid": "885a8cb3-c07b-4856-b448-eeb10bf36236"
12+
}
13+
},
14+
"domain": {
15+
"data": {
16+
"guid": "0b5f3633-194c-42d2-9408-972366617e0e"
17+
}
18+
}
19+
},
20+
"links": {
21+
"self": {
22+
"href": "https://api.example.org/v3/routes/cbad697f-cac1-48f4-9017-ac08f39dfb31"
23+
},
24+
"space": {
25+
"href": "https://api.example.org/v3/spaces/885a8cb3-c07b-4856-b448-eeb10bf36236"
26+
},
27+
"domain": {
28+
"href": "https://api.example.org/v3/domains/0b5f3633-194c-42d2-9408-972366617e0e"
29+
}
30+
}
31+
}
32+
<% end %>
33+
34+
<% content_for :paginated_list_of_routes do %>
35+
{
36+
"pagination": {
37+
"total_results": 3,
38+
"total_pages": 2,
39+
"first": {
40+
"href": "https://api.example.org/v3/routes?page=1&per_page=2"
41+
},
42+
"last": {
43+
"href": "https://api.example.org/v3/routes?page=2&per_page=2"
44+
},
45+
"next": {
46+
"href": "https://api.example.org/v3/routes?page=2&per_page=2"
47+
},
48+
"previous": null
49+
},
50+
"resources": [
51+
{
52+
"guid": "cbad697f-cac1-48f4-9017-ac08f39dfb31",
53+
"host": "a-hostname",
54+
"path": "/some_path",
55+
"created_at": "2019-05-10T17:17:48Z",
56+
"updated_at": "2019-05-10T17:17:48Z",
57+
"relationships": {
58+
"space": {
59+
"data": {
1160
"guid": "885a8cb3-c07b-4856-b448-eeb10bf36236"
12-
}
13-
},
14-
"domain": {
15-
"data": {
61+
}
62+
},
63+
"domain": {
64+
"data": {
1665
"guid": "0b5f3633-194c-42d2-9408-972366617e0e"
17-
}
18-
}
19-
},
20-
"links": {
21-
"self": {
22-
"href": "https://api.example.org/v3/routes/cbad697f-cac1-48f4-9017-ac08f39dfb31"
66+
}
67+
}
2368
},
24-
"space": {
25-
"href": "https://api.example.org/v3/spaces/885a8cb3-c07b-4856-b448-eeb10bf36236"
26-
},
27-
"domain": {
28-
"href": "https://api.example.org/v3/domains/0b5f3633-194c-42d2-9408-972366617e0e"
69+
"links": {
70+
"self": {
71+
"href": "https://api.example.org/v3/routes/cbad697f-cac1-48f4-9017-ac08f39dfb31"
72+
},
73+
"space": {
74+
"href": "https://api.example.org/v3/spaces/885a8cb3-c07b-4856-b448-eeb10bf36236"
75+
},
76+
"domain": {
77+
"href": "https://api.example.org/v3/domains/0b5f3633-194c-42d2-9408-972366617e0e"
78+
}
2979
}
30-
}
80+
}
81+
]
3182
}
3283
<% end %>
33-
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
### List routes
2+
3+
```
4+
Example Request
5+
```
6+
7+
```shell
8+
curl "https://api.example.org/v3/routes" \
9+
-X GET \
10+
-H "Authorization: bearer [token]"
11+
```
12+
13+
```
14+
Example Response
15+
```
16+
17+
```http
18+
HTTP/1.1 200 OK
19+
Content-Type: application/json
20+
21+
<%= yield_content :paginated_list_of_routes, '/v3/routes' %>
22+
```
23+
24+
Retrieve all routes the user has access to.
25+
26+
#### Definition
27+
`GET /v3/routes`
28+
29+
#### Query Parameters
30+
31+
Name | Type | Description
32+
---- | ---- | ------------
33+
**page** | _integer_ | Page to display. Valid values are integers >= 1.
34+
**per_page** | _integer_ | Number of results per page. <br>Valid values are 1 through 5000.
35+
**order_by** | _string_ | Value to sort by. Defaults to ascending. Prepend with `-` to sort descending. <br>Valid values are `created_at`, `updated_at`.
36+
37+
#### Permitted Roles
38+
|
39+
--- | ---
40+
All Roles |

Diff for: docs/v3/source/index.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ includes:
225225
- experimental_resources/routes/object
226226
- experimental_resources/routes/create
227227
- experimental_resources/routes/get
228+
- experimental_resources/routes/list
228229
- experimental_resources/service_bindings/header
229230
- experimental_resources/service_bindings/object
230231
- experimental_resources/service_bindings/create
@@ -233,8 +234,8 @@ includes:
233234
- experimental_resources/service_bindings/delete
234235
- experimental_resources/service_brokers/header
235236
- experimental_resources/service_brokers/object
236-
- experimental_resources/service_brokers/list
237237
- experimental_resources/service_brokers/get
238+
- experimental_resources/service_brokers/list
238239
- experimental_resources/sidecars/header
239240
- experimental_resources/sidecars/object
240241
- experimental_resources/sidecars/create_from_app

Diff for: spec/request/routes_spec.rb

+87
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,93 @@
77
let(:space) { VCAP::CloudController::Space.make }
88
let(:org) { space.organization }
99

10+
describe 'GET /v3/routes' do
11+
let(:other_space) { VCAP::CloudController::Space.make }
12+
let(:domain) { VCAP::CloudController::PrivateDomain.make(owning_organization: space.organization) }
13+
let(:route_in_org) { VCAP::CloudController::Route.make(space: space, domain: domain) }
14+
let(:route_in_other_org) { VCAP::CloudController::Route.make(space: other_space) }
15+
let(:api_call) { lambda { |user_headers| get '/v3/routes', nil, user_headers } }
16+
let(:route_in_org_json) do
17+
{
18+
guid: UUID_REGEX,
19+
host: route_in_org.host,
20+
path: route_in_org.path,
21+
created_at: iso8601,
22+
updated_at: iso8601,
23+
relationships: {
24+
space: {
25+
data: { guid: route_in_org.space.guid }
26+
},
27+
domain: {
28+
data: { guid: route_in_org.domain.guid }
29+
}
30+
},
31+
links: {
32+
self: { href: %r(#{Regexp.escape(link_prefix)}\/v3\/routes\/#{UUID_REGEX}) },
33+
space: { href: %r(#{Regexp.escape(link_prefix)}\/v3\/spaces\/#{route_in_org.space.guid}) },
34+
domain: { href: %r(#{Regexp.escape(link_prefix)}\/v3\/domains\/#{route_in_org.domain.guid}) }
35+
}
36+
}
37+
end
38+
39+
let(:route_in_other_org_json) do
40+
{
41+
guid: UUID_REGEX,
42+
host: route_in_other_org.host,
43+
path: route_in_other_org.path,
44+
created_at: iso8601,
45+
updated_at: iso8601,
46+
relationships: {
47+
space: {
48+
data: { guid: route_in_other_org.space.guid }
49+
},
50+
domain: {
51+
data: { guid: route_in_other_org.domain.guid }
52+
}
53+
},
54+
links: {
55+
self: { href: %r(#{Regexp.escape(link_prefix)}\/v3\/routes\/#{UUID_REGEX}) },
56+
space: { href: %r(#{Regexp.escape(link_prefix)}\/v3\/spaces\/#{route_in_other_org.space.guid}) },
57+
domain: { href: %r(#{Regexp.escape(link_prefix)}\/v3\/domains\/#{route_in_other_org.domain.guid}) }
58+
}
59+
}
60+
end
61+
62+
context 'when the user is a member in the routes org' do
63+
let(:expected_codes_and_responses) do
64+
h = Hash.new(
65+
code: 200,
66+
response_objects: [route_in_org_json]
67+
)
68+
69+
h['admin'] = { code: 200, response_objects: [route_in_org_json, route_in_other_org_json] }
70+
h['admin_read_only'] = { code: 200, response_objects: [route_in_org_json, route_in_other_org_json] }
71+
h['global_auditor'] = { code: 200, response_objects: [route_in_org_json, route_in_other_org_json] }
72+
73+
h['org_billing_manager'] = { code: 200, response_objects: [] }
74+
h['no_role'] = { code: 200, response_objects: [] }
75+
h
76+
end
77+
78+
it_behaves_like 'permissions for list endpoint', ALL_PERMISSIONS
79+
end
80+
81+
context 'when the request is invalid' do
82+
it 'returns 400 with a meaningful error' do
83+
get '/v3/routes?page=potato', nil, admin_header
84+
expect(last_response.status).to eq(400)
85+
expect(last_response).to have_error_message('The query parameter is invalid: Page must be a positive integer')
86+
end
87+
end
88+
89+
context 'when the user is not logged in' do
90+
it 'returns 401 for Unauthenticated requests' do
91+
get '/v3/routes', nil, base_json_headers
92+
expect(last_response.status).to eq(401)
93+
end
94+
end
95+
end
96+
1097
describe 'GET /v3/routes/:guid' do
1198
let(:domain) { VCAP::CloudController::PrivateDomain.make(owning_organization: space.organization) }
1299
let(:route) { VCAP::CloudController::Route.make(space: space, domain: domain) }

Diff for: spec/request_spec_shared_examples.rb

+9
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@
2828
if (200...300).cover? expected_response_code
2929
expected_response_objects = expected_codes_and_responses[role][:response_objects]
3030
expect({ resources: parsed_response['resources'] }).to match_json_response({ resources: expected_response_objects })
31+
32+
expect(parsed_response['pagination']).to match_json_response({
33+
total_results: an_instance_of(Integer),
34+
total_pages: an_instance_of(Integer),
35+
first: { href: /#{link_prefix}.+page=\d+&per_page=\d+/ },
36+
last: { href: /#{link_prefix}.+page=\d+&per_page=\d+/ },
37+
next: anything,
38+
previous: anything
39+
})
3140
end
3241
end
3342
end

Diff for: spec/unit/presenters/v3/droplet_presenter_spec.rb

+11-7
Original file line numberDiff line numberDiff line change
@@ -76,16 +76,20 @@ module VCAP::CloudController::Presenters::V3
7676
let(:buildpack_receipt_buildpack) { 'https://amelia:[email protected]' }
7777

7878
it 'obfuscates the username and password' do
79-
expect(result[:buildpacks]).to match_array([{ name: 'shaq',
80-
detect_output: nil,
81-
buildpack_name: nil,
82-
version: nil
83-
},
84-
{ name: 'https://***:***@neopets.com',
79+
expect(result[:buildpacks]).to match_array([
80+
{
81+
name: 'shaq',
82+
detect_output: nil,
83+
buildpack_name: nil,
84+
version: nil
85+
},
86+
{
87+
name: 'https://***:***@neopets.com',
8588
detect_output: 'the-happiest-buildpack-detect-output',
8689
buildpack_name: nil,
8790
version: nil
88-
}])
91+
}
92+
])
8993
end
9094
end
9195

0 commit comments

Comments
 (0)