Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions app/fetchers/droplet_list_fetcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,19 @@ def droplet_dataset(eager_loaded_associations, dataset=DropletModel.dataset)
end

def filter(message, app, space_guids, dataset)
if message.requested?(:current) && app
dataset = dataset.extension(:null_dataset)
return dataset.nullify unless app.droplet

dataset = dataset.where(guid: app.droplet_guid)
if message.requested?(:current)
if app
dataset = dataset.extension(:null_dataset)
return dataset.nullify unless app.droplet

dataset = dataset.where(guid: app.droplet_guid)
else
dataset = dataset.select_all(DropletModel.table_name).
join_table(:inner, AppModel.table_name, { droplet_guid: Sequel[DropletModel.table_name][:guid] }, { table_alias: :apps_current })
end
end

dataset = dataset.where(app_guid: message.app_guids) if message.requested?(:app_guids)
dataset = dataset.where(Sequel[DropletModel.table_name][:app_guid] => message.app_guids) if message.requested?(:app_guids)

dataset = dataset.where(state: message.states) if message.requested?(:states)

Expand Down
16 changes: 8 additions & 8 deletions app/messages/droplets_list_message.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ class DropletsListMessage < MetadataListMessage
validates :states, array: true, allow_nil: true
validates :space_guids, array: true, allow_nil: true
validates :organization_guids, array: true, allow_nil: true
validates :current, inclusion: { in: ['true'], message: 'only accepts the value \'true\'' }, allow_nil: true, if: -> { app_guid.present? }
validates :current, inclusion: { in: ['true'], message: 'only accepts the value \'true\'' }, allow_nil: true
validate :app_nested_request, if: -> { app_guid.present? }
validate :not_app_nested_request, unless: -> { app_guid.present? }
validate :package_nested_request, if: -> { package_guid.present? }

def to_param_hash
super(exclude: %i[app_guid package_guid])
Expand All @@ -32,18 +32,18 @@ def self.from_params(params)

private

def not_app_nested_request

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we convert that method into a package-specific validator rather than removing it?
not_app_nested_request gated all routes where app_guid is absent, that's both the global GET /v3/droplets route and the package-nested GET /v3/packages/:guid/droplets route. Removing it enables current=true on the global route as intended, but also silently enables it on the package route, which flips that endpoint from returning 422 Unknown query parameter to 200 with a join executed. That's outside the PR's stated scope and untested. A package_nested_request validator (firing on package_guid.present?) would still reject current there while allowing it on the global route.

invalid_attributes = []
invalid_attributes << :current if current
errors.add(:base, "Unknown query parameter(s): '#{invalid_attributes.join("', '")}'") if invalid_attributes.present?
end

def app_nested_request
invalid_attributes = []
invalid_attributes << :app_guids if app_guids
invalid_attributes << :organization_guids if organization_guids
invalid_attributes << :space_guids if space_guids
errors.add(:base, "Unknown query parameter(s): '#{invalid_attributes.join("', '")}'") if invalid_attributes.present?
end

def package_nested_request
invalid_attributes = []
invalid_attributes << :current if current
errors.add(:base, "Unknown query parameter(s): '#{invalid_attributes.join("', '")}'") if invalid_attributes.present?
end
end
end
1 change: 1 addition & 0 deletions docs/v3/source/includes/resources/droplets/_list.md.erb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Name | Type | Description
**guids** | _list of strings_ | Comma-delimited list of droplet guids to filter by
**states** | _list of strings_ | Comma-delimited list of droplet states to filter by
**app_guids** | _list of strings_ | Comma-delimited list of app guids to filter by
**current** | _boolean_ | If true, only include the current droplet for each app
**space_guids** | _list of strings_ | Comma-delimited list of space guids to filter by
**organization_guids** | _list of strings_ | Comma-delimited list of organization guids to filter by
**page** | _integer_ | Page to display; valid values are integers >= 1
Expand Down
46 changes: 45 additions & 1 deletion spec/request/droplets_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,7 @@
space_guids
app_guids
organization_guids
current

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This moves current into the valid-params list, but there's no request-level test that actually calls GET /v3/droplets?current=true and asserts the response returns only current droplets. The filtering is covered at the fetcher unit level, but a request spec for the happy path would cover the full controller→message→fetcher→presenter path. Non-blocking, but worth adding.

]
end
let(:params) do
Expand All @@ -668,7 +669,6 @@
order_by: 'updated_at',
guids: 'foo,bar',
app_guid: app_model.guid,
current: true,
package_guid: package_model.guid,
states: %w[test foo],
label_selector: 'foo,bar',
Expand Down Expand Up @@ -941,6 +941,50 @@
returned_guids = parsed_response['resources'].pluck('guid')
expect(returned_guids).to contain_exactly(droplet1.guid, droplet2.guid, droplet3.guid)
end

it 'filters by current=true as admin' do
current_droplet_app1 = create(:droplet_model, app: app_model, state: VCAP::CloudController::DropletModel::STAGED_STATE)
current_droplet_app2 = create(:droplet_model, app: app_model2, state: VCAP::CloudController::DropletModel::STAGED_STATE)
current_droplet_app3 = create(:droplet_model, app: app_model3, state: VCAP::CloudController::DropletModel::STAGED_STATE)
app_model.update(droplet: current_droplet_app1)
app_model2.update(droplet: current_droplet_app2)
app_model3.update(droplet: current_droplet_app3)

get '/v3/droplets?current=true', nil, admin_headers

expect(last_response.status).to eq(200)
returned_guids = parsed_response['resources'].pluck('guid')
expect(returned_guids).to contain_exactly(current_droplet_app1.guid, current_droplet_app2.guid, current_droplet_app3.guid)
end

it 'filters by current=true as a non-admin developer, returning only current droplets in readable spaces' do
current_droplet_app1 = create(:droplet_model, app: app_model, state: VCAP::CloudController::DropletModel::STAGED_STATE)
current_droplet_app2 = create(:droplet_model, app: app_model2, state: VCAP::CloudController::DropletModel::STAGED_STATE)
current_droplet_app3 = create(:droplet_model, app: app_model3, state: VCAP::CloudController::DropletModel::STAGED_STATE)
app_model.update(droplet: current_droplet_app1)
app_model2.update(droplet: current_droplet_app2)
app_model3.update(droplet: current_droplet_app3)

get '/v3/droplets?current=true', nil, developer_headers

expect(last_response.status).to eq(200)
returned_guids = parsed_response['resources'].pluck('guid')
expect(returned_guids).to contain_exactly(current_droplet_app1.guid, current_droplet_app2.guid)
expect(returned_guids).not_to include(current_droplet_app3.guid)
end

it 'filters by current=true combined with app_guids' do
current_droplet_app1 = create(:droplet_model, app: app_model, state: VCAP::CloudController::DropletModel::STAGED_STATE)
current_droplet_app2 = create(:droplet_model, app: app_model2, state: VCAP::CloudController::DropletModel::STAGED_STATE)
app_model.update(droplet: current_droplet_app1)
app_model2.update(droplet: current_droplet_app2)

get "/v3/droplets?current=true&app_guids=#{app_model.guid}", nil, developer_headers

expect(last_response.status).to eq(200)
returned_guids = parsed_response['resources'].pluck('guid')
expect(returned_guids).to contain_exactly(current_droplet_app1.guid)
end
end

context 'label_selector' do
Expand Down
70 changes: 70 additions & 0 deletions spec/unit/fetchers/droplet_list_fetcher_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,76 @@ module VCAP::CloudController
expect(results).to contain_exactly(staged_droplet_for_app1)
end
end

context 'filtering by current=true' do
let(:filters) { { current: 'true' } }

context 'when some apps have a current droplet set' do
before do
app1.update(droplet: staged_droplet_for_app1)
app2.update(droplet: staged_droplet_for_app2)
end

it 'returns only current droplets' do
results = fetcher.fetch_all(message).all
expect(results).to contain_exactly(staged_droplet_for_app1, staged_droplet_for_app2)
end

it 'does not return non-current droplets' do
results = fetcher.fetch_all(message).all
expect(results).not_to include(failed_droplet_for_app1)
end
end

context 'when no apps have a current droplet set' do
it 'returns an empty list' do
results = fetcher.fetch_all(message).all
expect(results).to be_empty
end
end

context 'when combined with app_guids filter' do
before do
app1.update(droplet: staged_droplet_for_app1)
app2.update(droplet: staged_droplet_for_app2)
end

let(:filters) { { current: 'true', app_guids: [app1.guid] } }

it 'returns only the current droplet for the specified app' do
results = fetcher.fetch_all(message).all
expect(results).to contain_exactly(staged_droplet_for_app1)
end
end

context 'when combined with space_guids filter' do
before do
app1.update(droplet: staged_droplet_for_app1)
app2.update(droplet: staged_droplet_for_app2)
end

let(:filters) { { current: 'true', space_guids: [app1.space.guid] } }

it 'returns only current droplets in the specified space' do
results = fetcher.fetch_all(message).all
expect(results).to contain_exactly(staged_droplet_for_app1)
end
end

context 'when combined with organization_guids filter' do
before do
app1.update(droplet: staged_droplet_for_app1)
app2.update(droplet: staged_droplet_for_app2)
end

let(:filters) { { current: 'true', organization_guids: [app1.organization.guid] } }

it 'returns only current droplets in the specified organization' do
results = fetcher.fetch_all(message).all
expect(results).to contain_exactly(staged_droplet_for_app1)
end
end
end
end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

current=true combined with organization_guids or space_guids isn't covered, those add a second join to apps (apps_orgs/apps_spaces alongside the new apps_current). Could we add fetcher tests for those two combinations?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added


describe '#fetch_for_spaces' do
Expand Down
17 changes: 16 additions & 1 deletion spec/unit/messages/droplets_list_message_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,23 @@ module VCAP::CloudController

context 'when the query is not nested under an app' do

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a test asserting current is still rejected on the package-nested route (package_guid present, app_guid absent)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

context 'when the request contains current field' do
it 'is invalid' do
it 'is valid' do
message = DropletsListMessage.from_params({ current: 'true' })
expect(message).to be_valid
end

it 'validates current must be true' do
message = DropletsListMessage.from_params({ current: 'false' })
expect(message).not_to be_valid
expect(message.errors[:current]).to include("only accepts the value 'true'")
end
end
end

context 'when the query is nested under a package' do
context 'when the request contains current field' do
it 'is invalid' do
message = DropletsListMessage.from_params({ package_guid: 'some-package-guid', current: 'true' })
expect(message).not_to be_valid
expect(message.errors[:base][0]).to include("Unknown query parameter(s): 'current'")
end
Expand Down
Loading