spec/request/packages_spec.rb
require 'spec_helper'
require 'request_spec_shared_examples'
RSpec.describe 'Packages' do
let(:email) { 'potato@house.com' }
let(:user) { VCAP::CloudController::User.make }
let(:user_name) { 'clarence' }
let(:user_header) { headers_for(user, email:, user_name:) }
let(:org) { VCAP::CloudController::Organization.make(created_at: 3.days.ago) }
let(:space) { VCAP::CloudController::Space.make(organization: org) }
let(:space_guid) { space.guid }
let(:app_model) { VCAP::CloudController::AppModel.make(:docker, space_guid:) }
describe 'POST /v3/packages' do
let(:guid) { app_model.guid }
before do
space.organization.add_user(user)
space.add_developer(user)
end
let(:type) { 'docker' }
let(:data) { { image: 'registry/image:latest', username: 'my-docker-username', password: 'my-password' } }
let(:expected_data) { { image: 'registry/image:latest', username: 'my-docker-username', password: '***' } }
let(:relationships) { { app: { data: { guid: app_model.guid } } } }
let(:metadata) do
{
labels: {
release: 'stable',
'seriouseats.com/potato' => 'mashed'
},
annotations: {
potato: 'idaho'
}
}
end
describe 'creation' do
it 'creates a package' do
expect do
post '/v3/packages', { type:, data:, relationships:, metadata: }.to_json, user_header
end.to change(VCAP::CloudController::PackageModel, :count).by(1)
package = VCAP::CloudController::PackageModel.last
expected_response = {
'guid' => package.guid,
'type' => type,
'data' => {
'image' => 'registry/image:latest',
'username' => 'my-docker-username',
'password' => '***'
},
'state' => 'READY',
'relationships' => { 'app' => { 'data' => { 'guid' => app_model.guid } } },
'metadata' => { 'labels' => { 'release' => 'stable', 'seriouseats.com/potato' => 'mashed' }, 'annotations' => { 'potato' => 'idaho' } },
'created_at' => iso8601,
'updated_at' => iso8601,
'links' => {
'self' => { 'href' => "#{link_prefix}/v3/packages/#{package.guid}" },
'app' => { 'href' => "#{link_prefix}/v3/apps/#{guid}" }
}
}
expected_event_metadata = {
package_guid: package.guid,
request: {
type: type,
data: expected_data,
relationships: relationships,
metadata: metadata
}
}.to_json
parsed_response = Oj.load(last_response.body)
expect(last_response.status).to eq(201)
expect(parsed_response).to be_a_response_like(expected_response)
event = VCAP::CloudController::Event.last
expect(event.values).to include({
type: 'audit.app.package.create',
actor: user.guid,
actor_type: 'user',
actor_name: email,
actor_username: user_name,
actee: package.app.guid,
actee_type: 'app',
actee_name: package.app.name,
metadata: expected_event_metadata,
space_guid: space.guid,
organization_guid: space.organization.guid
})
end
context 'permissions' do
before do
space.remove_developer(user)
space.organization.remove_user(user)
end
let(:api_call) { ->(user_headers) { post '/v3/packages', { type:, data:, relationships:, metadata: }.to_json, user_headers } }
let(:space) { VCAP::CloudController::Space.make }
let(:org) { space.organization }
let(:user) { VCAP::CloudController::User.make }
let(:expected_codes_and_responses) do
h = Hash.new(code: 201)
h['org_auditor'] = { code: 422 }
h['org_billing_manager'] = { code: 422 }
h['no_role'] = { code: 422 }
%w[admin_read_only global_auditor space_manager space_auditor org_manager space_supporter].each do |r|
h[r] = { code: 403, errors: CF_NOT_AUTHORIZED }
end
h
end
it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
context 'when organization is suspended' do
let(:expected_codes_and_responses) do
h = super()
h['space_developer'] = { code: 403, errors: CF_ORG_SUSPENDED }
h
end
before do
org.update(status: VCAP::CloudController::Organization::SUSPENDED)
end
it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
end
end
end
describe 'copying' do
let!(:source_package) { VCAP::CloudController::PackageModel.make(type: 'docker', app_guid: app_model.guid, docker_image: 'http://awesome-sauce.com') }
let(:target_space) { space }
let(:target_org) { target_space.organization }
let(:target_app) { VCAP::CloudController::AppModel.make(space: target_space) }
it 'copies a package' do
expect do
post "/v3/packages?source_guid=#{source_package.guid}",
{
relationships: {
app: { data: { guid: target_app.guid } }
}
}.to_json,
user_header
end.to change(VCAP::CloudController::PackageModel, :count).by(1)
package = VCAP::CloudController::PackageModel.last
expected_response = {
'guid' => package.guid,
'type' => 'docker',
'data' => {
'image' => 'http://awesome-sauce.com',
'username' => nil,
'password' => nil
},
'state' => 'READY',
'relationships' => { 'app' => { 'data' => { 'guid' => target_app.guid } } },
'metadata' => { 'labels' => {}, 'annotations' => {} },
'created_at' => iso8601,
'updated_at' => iso8601,
'links' => {
'self' => { 'href' => "#{link_prefix}/v3/packages/#{package.guid}" },
'app' => { 'href' => "#{link_prefix}/v3/apps/#{target_app.guid}" }
}
}
expect(last_response.status).to eq(201)
parsed_response = Oj.load(last_response.body)
expect(parsed_response).to be_a_response_like(expected_response)
expected_event_metadata = {
package_guid: package.guid,
request: {
source_package_guid: source_package.guid
}
}.to_json
event = VCAP::CloudController::Event.last
expect(event.values).to include({
type: 'audit.app.package.create',
actor: user.guid,
actor_type: 'user',
actor_name: email,
actor_username: user_name,
actee: package.app.guid,
actee_type: 'app',
actee_name: package.app.name,
metadata: expected_event_metadata,
space_guid: space.guid,
organization_guid: space.organization.guid
})
end
context 'permissions' do
before do
space.remove_developer(user)
space.organization.remove_user(user)
end
let(:api_call) do
lambda { |user_headers|
post "/v3/packages?source_guid=#{source_package.guid}",
{
relationships: {
app: { data: { guid: target_app.guid } }
}
}.to_json, user_headers
}
end
let(:expected_codes_and_responses) do
h = Hash.new(code: 201)
h['org_auditor'] = { code: 422 }
h['org_billing_manager'] = { code: 422 }
h['no_role'] = { code: 422 }
%w[admin_read_only global_auditor space_manager space_auditor org_manager space_supporter].each do |r|
h[r] = { code: 403, errors: CF_NOT_AUTHORIZED }
end
h
end
it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
context 'when target organization is suspended' do
let(:expected_codes_and_responses) do
h = super()
h['space_developer'] = { code: 403, errors: CF_ORG_SUSPENDED }
h
end
before do
target_org.update(status: VCAP::CloudController::Organization::SUSPENDED)
end
it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
end
context 'when source organization is suspended' do
let(:target_space) { VCAP::CloudController::Space.make(organization: VCAP::CloudController::Organization.make) }
let(:source_org) { org }
let(:expected_codes_and_responses) do
h = super()
h['space_developer'] = { code: 403, errors: CF_ORG_SUSPENDED }
h
end
before do
target_org.add_user(user)
target_space.add_developer(user)
source_org.update(status: VCAP::CloudController::Organization::SUSPENDED)
end
it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
end
end
end
end
describe 'GET /v3/apps/:guid/packages' do
let!(:package) { VCAP::CloudController::PackageModel.make(app_guid: app_model.guid) }
let(:app_model) { VCAP::CloudController::AppModel.make(space_guid: space.guid) }
let(:guid) { app_model.guid }
let(:page) { 1 }
let(:per_page) { 2 }
let(:order_by) { '-created_at' }
context 'when listing all packages for an app' do
let(:api_call) { ->(user_headers) { get "/v3/apps/#{app_model.guid}/packages", nil, user_headers } }
let(:packages_response_objects) do
[
{
'guid' => package.guid,
'type' => 'bits',
'data' => {
'checksum' => { 'type' => 'sha256', 'value' => anything },
'error' => nil
},
'relationships' => { 'app' => { 'data' => { 'guid' => app_model.guid } } },
'state' => VCAP::CloudController::PackageModel::CREATED_STATE,
'metadata' => { 'labels' => {}, 'annotations' => {} },
'created_at' => iso8601,
'updated_at' => iso8601,
'links' => {
'self' => { 'href' => "#{link_prefix}/v3/packages/#{package.guid}" },
'upload' => { 'href' => "#{link_prefix}/v3/packages/#{package.guid}/upload", 'method' => 'POST' },
'download' => { 'href' => "#{link_prefix}/v3/packages/#{package.guid}/download" },
'app' => { 'href' => "#{link_prefix}/v3/apps/#{guid}" }
}
}
]
end
let(:expected_codes_and_responses) do
h = Hash.new(code: 200, response_objects: packages_response_objects)
h['org_auditor'] = { code: 404 }
h['org_billing_manager'] = { code: 404 }
h['no_role'] = { code: 404 }
h
end
it_behaves_like 'permissions for list endpoint', ALL_PERMISSIONS
end
context 'when listing paginated results of all packages for an app' do
before do
space.organization.add_user(user)
space.add_developer(user)
end
it 'lists paginated results' do
package2 = VCAP::CloudController::PackageModel.make(app_guid: app_model.guid, created_at: package.created_at + 1.hour)
expected_response = {
'pagination' => {
'total_results' => 2,
'total_pages' => 1,
'first' => { 'href' => "#{link_prefix}/v3/apps/#{guid}/packages?order_by=-created_at&page=1&per_page=2" },
'last' => { 'href' => "#{link_prefix}/v3/apps/#{guid}/packages?order_by=-created_at&page=1&per_page=2" },
'next' => nil,
'previous' => nil
},
'resources' => [
{
'guid' => package2.guid,
'type' => 'bits',
'data' => {
'checksum' => { 'type' => 'sha256', 'value' => anything },
'error' => nil
},
'relationships' => { 'app' => { 'data' => { 'guid' => app_model.guid } } },
'state' => VCAP::CloudController::PackageModel::CREATED_STATE,
'metadata' => { 'labels' => {}, 'annotations' => {} },
'created_at' => iso8601,
'updated_at' => iso8601,
'links' => {
'self' => { 'href' => "#{link_prefix}/v3/packages/#{package2.guid}" },
'upload' => { 'href' => "#{link_prefix}/v3/packages/#{package2.guid}/upload", 'method' => 'POST' },
'download' => { 'href' => "#{link_prefix}/v3/packages/#{package2.guid}/download" },
'app' => { 'href' => "#{link_prefix}/v3/apps/#{guid}" }
}
},
{
'guid' => package.guid,
'type' => 'bits',
'data' => {
'checksum' => { 'type' => 'sha256', 'value' => anything },
'error' => nil
},
'relationships' => { 'app' => { 'data' => { 'guid' => app_model.guid } } },
'state' => VCAP::CloudController::PackageModel::CREATED_STATE,
'metadata' => { 'labels' => {}, 'annotations' => {} },
'created_at' => iso8601,
'updated_at' => iso8601,
'links' => {
'self' => { 'href' => "#{link_prefix}/v3/packages/#{package.guid}" },
'upload' => { 'href' => "#{link_prefix}/v3/packages/#{package.guid}/upload", 'method' => 'POST' },
'download' => { 'href' => "#{link_prefix}/v3/packages/#{package.guid}/download" },
'app' => { 'href' => "#{link_prefix}/v3/apps/#{guid}" }
}
}
]
}
get "/v3/apps/#{guid}/packages?page=#{page}&per_page=#{per_page}&order_by=#{order_by}", {}, user_header
parsed_response = Oj.load(last_response.body)
expect(last_response.status).to eq(200)
expect(parsed_response).to be_a_response_like(expected_response)
end
it_behaves_like 'list_endpoint_with_common_filters' do
let(:resource_klass) { VCAP::CloudController::PackageModel }
let(:api_call) do
->(headers, filters) { get "/v3/apps/#{guid}/packages?#{filters}", nil, headers }
end
let(:additional_resource_params) { { app: app_model } }
let(:headers) { admin_headers }
end
end
context 'when listing a subset of packages for an app' do
before do
space.organization.add_user(user)
space.add_developer(user)
end
it 'filters by types' do
VCAP::CloudController::PackageModel.make(app_guid: app_model.guid, type: VCAP::CloudController::PackageModel::BITS_TYPE)
VCAP::CloudController::PackageModel.make(app_guid: app_model.guid, type: VCAP::CloudController::PackageModel::BITS_TYPE)
VCAP::CloudController::PackageModel.make(app_guid: app_model.guid, type: VCAP::CloudController::PackageModel::DOCKER_TYPE)
VCAP::CloudController::PackageModel.make(type: VCAP::CloudController::PackageModel::BITS_TYPE)
get '/v3/packages?types=bits', {}, user_header
expected_pagination = {
'total_results' => 3,
'total_pages' => 1,
'first' => { 'href' => "#{link_prefix}/v3/packages?page=1&per_page=50&types=bits" },
'last' => { 'href' => "#{link_prefix}/v3/packages?page=1&per_page=50&types=bits" },
'next' => nil,
'previous' => nil
}
parsed_response = Oj.load(last_response.body)
expect(last_response.status).to eq(200)
expect(parsed_response['resources'].count).to eq(3)
expect(parsed_response['resources'].pluck('type').uniq).to eq(['bits'])
expect(parsed_response['pagination']).to eq(expected_pagination)
end
it 'filters by states' do
VCAP::CloudController::PackageModel.make(app_guid: app_model.guid, state: VCAP::CloudController::PackageModel::PENDING_STATE)
VCAP::CloudController::PackageModel.make(app_guid: app_model.guid, state: VCAP::CloudController::PackageModel::PENDING_STATE)
VCAP::CloudController::PackageModel.make(app_guid: app_model.guid, state: VCAP::CloudController::PackageModel::READY_STATE)
VCAP::CloudController::PackageModel.make(state: VCAP::CloudController::PackageModel::PENDING_STATE)
get "/v3/apps/#{app_model.guid}/packages?states=PROCESSING_UPLOAD", {}, user_header
expected_pagination = {
'total_results' => 2,
'total_pages' => 1,
'first' => { 'href' => "#{link_prefix}/v3/apps/#{app_model.guid}/packages?page=1&per_page=50&states=PROCESSING_UPLOAD" },
'last' => { 'href' => "#{link_prefix}/v3/apps/#{app_model.guid}/packages?page=1&per_page=50&states=PROCESSING_UPLOAD" },
'next' => nil,
'previous' => nil
}
parsed_response = Oj.load(last_response.body)
expect(last_response.status).to eq(200)
expect(parsed_response['resources'].count).to eq(2)
expect(parsed_response['resources'].pluck('state').uniq).to eq(['PROCESSING_UPLOAD'])
expect(parsed_response['pagination']).to eq(expected_pagination)
end
it 'filters by package guids' do
package1 = VCAP::CloudController::PackageModel.make(app_guid: app_model.guid)
package2 = VCAP::CloudController::PackageModel.make(app_guid: app_model.guid)
VCAP::CloudController::PackageModel.make
get "/v3/apps/#{app_model.guid}/packages?guids=#{package1.guid},#{package2.guid}", {}, user_header
expected_pagination = {
'total_results' => 2,
'total_pages' => 1,
'first' => { 'href' => "#{link_prefix}/v3/apps/#{app_model.guid}/packages?guids=#{package1.guid}%2C#{package2.guid}&page=1&per_page=50" },
'last' => { 'href' => "#{link_prefix}/v3/apps/#{app_model.guid}/packages?guids=#{package1.guid}%2C#{package2.guid}&page=1&per_page=50" },
'next' => nil,
'previous' => nil
}
parsed_response = Oj.load(last_response.body)
expect(last_response.status).to eq(200)
expect(parsed_response['resources'].pluck('guid')).to contain_exactly(package1.guid, package2.guid)
expect(parsed_response['pagination']).to eq(expected_pagination)
end
end
end
describe 'GET /v3/packages' do
let(:bits_type) { 'bits' }
let(:docker_type) { 'docker' }
let(:page) { 1 }
let(:per_page) { 2 }
let(:params) do
{
guids: %w[foo bar],
space_guids: %w[foo bar],
organization_guids: %w[foo bar],
app_guids: %w[foo bar],
states: %w[foo bar],
types: %w[foo bar],
page: '2',
per_page: '10',
order_by: 'updated_at',
label_selector: 'foo,bar',
created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}",
updated_ats: { gt: Time.now.utc.iso8601 }
}
end
let(:bits_package) { VCAP::CloudController::PackageModel.make(type: bits_type, app_guid: app_model.guid) }
let(:docker_package) do
VCAP::CloudController::PackageModel.make(
type: docker_type,
app_guid: app_model.guid,
state: VCAP::CloudController::PackageModel::READY_STATE,
docker_image: 'http://location-of-image.com'
)
end
let(:packages_response_objects) do
[
{
'guid' => bits_package.guid,
'type' => 'bits',
'data' => {
'checksum' => { 'type' => 'sha256', 'value' => anything },
'error' => nil
},
'state' => VCAP::CloudController::PackageModel::CREATED_STATE,
'relationships' => { 'app' => { 'data' => { 'guid' => app_model.guid } } },
'metadata' => { 'labels' => {}, 'annotations' => {} },
'created_at' => iso8601,
'updated_at' => iso8601,
'links' => {
'self' => { 'href' => "#{link_prefix}/v3/packages/#{bits_package.guid}" },
'upload' => { 'href' => "#{link_prefix}/v3/packages/#{bits_package.guid}/upload", 'method' => 'POST' },
'download' => { 'href' => "#{link_prefix}/v3/packages/#{bits_package.guid}/download" },
'app' => { 'href' => "#{link_prefix}/v3/apps/#{bits_package.app_guid}" }
}
},
{
'guid' => docker_package.guid,
'type' => 'docker',
'data' => {
'image' => 'http://location-of-image.com',
'username' => nil,
'password' => nil
},
'state' => VCAP::CloudController::PackageModel::READY_STATE,
'relationships' => { 'app' => { 'data' => { 'guid' => app_model.guid } } },
'metadata' => { 'labels' => {}, 'annotations' => {} },
'created_at' => iso8601,
'updated_at' => iso8601,
'links' => {
'self' => { 'href' => "#{link_prefix}/v3/packages/#{docker_package.guid}" },
'app' => { 'href' => "#{link_prefix}/v3/apps/#{docker_package.app_guid}" }
}
}
]
end
context 'when listing all packages' do
let(:api_call) { ->(user_headers) { get '/v3/packages', { per_page: }, user_headers } }
let(:message) { VCAP::CloudController::PackagesListMessage }
let(:request) { '/v3/packages' }
let(:excluded_params) do
[
:app_guid
]
end
let(:expected_codes_and_responses) do
h = Hash.new(code: 200, response_objects: packages_response_objects)
h['org_auditor'] = { code: 200, response_objects: [] }
h['org_billing_manager'] = { code: 200, response_objects: [] }
h['no_role'] = { code: 200, response_objects: [] }
h
end
it_behaves_like 'permissions for list endpoint', ALL_PERMISSIONS
end
context 'when listing a subset of packages' do
before do
space.organization.add_user(user)
space.add_developer(user)
end
it_behaves_like 'list query endpoint' do
let(:message) { VCAP::CloudController::PackagesListMessage }
let(:request) { '/v3/packages' }
let(:excluded_params) do
[
:app_guid
]
end
end
it_behaves_like 'list_endpoint_with_common_filters' do
let(:resource_klass) { VCAP::CloudController::PackageModel }
let(:api_call) do
->(headers, filters) { get "/v3/packages?#{filters}", nil, headers }
end
let(:headers) { admin_headers }
end
context 'faceted search' do
it 'filters by types' do
VCAP::CloudController::PackageModel.make(app_guid: app_model.guid, type: VCAP::CloudController::PackageModel::BITS_TYPE)
VCAP::CloudController::PackageModel.make(app_guid: app_model.guid, type: VCAP::CloudController::PackageModel::BITS_TYPE)
VCAP::CloudController::PackageModel.make(app_guid: app_model.guid, type: VCAP::CloudController::PackageModel::DOCKER_TYPE)
another_app_in_same_space = VCAP::CloudController::AppModel.make(space_guid:)
VCAP::CloudController::PackageModel.make(app_guid: another_app_in_same_space.guid, type: VCAP::CloudController::PackageModel::BITS_TYPE)
get '/v3/packages?types=bits', {}, user_header
expected_pagination = {
'total_results' => 3,
'total_pages' => 1,
'first' => { 'href' => "#{link_prefix}/v3/packages?page=1&per_page=50&types=bits" },
'last' => { 'href' => "#{link_prefix}/v3/packages?page=1&per_page=50&types=bits" },
'next' => nil,
'previous' => nil
}
parsed_response = Oj.load(last_response.body)
expect(last_response.status).to eq(200)
expect(parsed_response['resources'].count).to eq(3)
expect(parsed_response['resources'].pluck('type').uniq).to eq(['bits'])
expect(parsed_response['pagination']).to eq(expected_pagination)
end
it 'filters by states' do
VCAP::CloudController::PackageModel.make(app_guid: app_model.guid, state: VCAP::CloudController::PackageModel::PENDING_STATE)
VCAP::CloudController::PackageModel.make(app_guid: app_model.guid, state: VCAP::CloudController::PackageModel::PENDING_STATE)
VCAP::CloudController::PackageModel.make(app_guid: app_model.guid, state: VCAP::CloudController::PackageModel::READY_STATE)
another_app_in_same_space = VCAP::CloudController::AppModel.make(space_guid:)
VCAP::CloudController::PackageModel.make(app_guid: another_app_in_same_space.guid, state: VCAP::CloudController::PackageModel::PENDING_STATE)
get '/v3/packages?states=PROCESSING_UPLOAD', {}, user_header
expected_pagination = {
'total_results' => 3,
'total_pages' => 1,
'first' => { 'href' => "#{link_prefix}/v3/packages?page=1&per_page=50&states=PROCESSING_UPLOAD" },
'last' => { 'href' => "#{link_prefix}/v3/packages?page=1&per_page=50&states=PROCESSING_UPLOAD" },
'next' => nil,
'previous' => nil
}
parsed_response = Oj.load(last_response.body)
expect(last_response.status).to eq(200)
expect(parsed_response['resources'].count).to eq(3)
expect(parsed_response['resources'].pluck('state').uniq).to eq(['PROCESSING_UPLOAD'])
expect(parsed_response['pagination']).to eq(expected_pagination)
end
it 'filters by app guids' do
app_model2 = VCAP::CloudController::AppModel.make(space_guid:)
package1 = VCAP::CloudController::PackageModel.make(app_guid: app_model.guid)
package2 = VCAP::CloudController::PackageModel.make(app_guid: app_model2.guid)
VCAP::CloudController::PackageModel.make
get "/v3/packages?app_guids=#{app_model.guid},#{app_model2.guid}", {}, user_header
expected_pagination = {
'total_results' => 2,
'total_pages' => 1,
'first' => { 'href' => "#{link_prefix}/v3/packages?app_guids=#{app_model.guid}%2C#{app_model2.guid}&page=1&per_page=50" },
'last' => { 'href' => "#{link_prefix}/v3/packages?app_guids=#{app_model.guid}%2C#{app_model2.guid}&page=1&per_page=50" },
'next' => nil,
'previous' => nil
}
parsed_response = Oj.load(last_response.body)
expect(last_response.status).to eq(200)
expect(parsed_response['resources'].pluck('guid')).to contain_exactly(package1.guid, package2.guid)
expect(parsed_response['pagination']).to eq(expected_pagination)
end
it 'filters by package guids' do
app_model2 = VCAP::CloudController::AppModel.make(space_guid:)
package1 = VCAP::CloudController::PackageModel.make(app_guid: app_model2.guid)
package2 = VCAP::CloudController::PackageModel.make(app_guid: app_model2.guid)
VCAP::CloudController::PackageModel.make
get "/v3/packages?guids=#{package1.guid},#{package2.guid}", {}, user_header
expected_pagination = {
'total_results' => 2,
'total_pages' => 1,
'first' => { 'href' => "#{link_prefix}/v3/packages?guids=#{package1.guid}%2C#{package2.guid}&page=1&per_page=50" },
'last' => { 'href' => "#{link_prefix}/v3/packages?guids=#{package1.guid}%2C#{package2.guid}&page=1&per_page=50" },
'next' => nil,
'previous' => nil
}
parsed_response = Oj.load(last_response.body)
expect(last_response.status).to eq(200)
expect(parsed_response['resources'].pluck('guid')).to contain_exactly(package1.guid, package2.guid)
expect(parsed_response['pagination']).to eq(expected_pagination)
end
it 'filters by space guids' do
package_on_space1 = VCAP::CloudController::PackageModel.make(app_guid: app_model.guid)
space2 = VCAP::CloudController::Space.make(organization: space.organization)
space2.add_developer(user)
app_model2 = VCAP::CloudController::AppModel.make(space_guid: space2.guid)
package_on_space2 = VCAP::CloudController::PackageModel.make(app_guid: app_model2.guid)
space3 = VCAP::CloudController::Space.make(organization: space.organization)
space3.add_developer(user)
app_model3 = VCAP::CloudController::AppModel.make(space_guid: space3.guid)
VCAP::CloudController::PackageModel.make(app_guid: app_model3.guid)
get "/v3/packages?space_guids=#{space2.guid},#{space_guid}", {}, user_header
expected_pagination = {
'total_results' => 2,
'total_pages' => 1,
'first' => { 'href' => "#{link_prefix}/v3/packages?page=1&per_page=50&space_guids=#{space2.guid}%2C#{space_guid}" },
'last' => { 'href' => "#{link_prefix}/v3/packages?page=1&per_page=50&space_guids=#{space2.guid}%2C#{space_guid}" },
'next' => nil,
'previous' => nil
}
parsed_response = Oj.load(last_response.body)
expect(last_response.status).to eq(200)
expect(parsed_response['resources'].pluck('guid')).to contain_exactly(package_on_space2.guid, package_on_space1.guid)
expect(parsed_response['pagination']).to eq(expected_pagination)
end
it 'filters by org guids' do
org1_guid = space.organization.guid
package_in_org1 = VCAP::CloudController::PackageModel.make(app_guid: app_model.guid)
space2 = VCAP::CloudController::Space.make
org2_guid = space2.organization.guid
app_model2 = VCAP::CloudController::AppModel.make(space_guid: space2.guid)
space2.organization.add_user(user)
space2.add_developer(user)
package_in_org2 = VCAP::CloudController::PackageModel.make(app_guid: app_model2.guid)
space3 = VCAP::CloudController::Space.make
space3.organization.add_user(user)
space3.add_developer(user)
app_model3 = VCAP::CloudController::AppModel.make(space_guid: space3.guid)
VCAP::CloudController::PackageModel.make(app_guid: app_model3.guid)
get "/v3/packages?organization_guids=#{org1_guid},#{org2_guid}", {}, user_header
expected_pagination = {
'total_results' => 2,
'total_pages' => 1,
'first' => { 'href' => "#{link_prefix}/v3/packages?organization_guids=#{org1_guid}%2C#{org2_guid}&page=1&per_page=50" },
'last' => { 'href' => "#{link_prefix}/v3/packages?organization_guids=#{org1_guid}%2C#{org2_guid}&page=1&per_page=50" },
'next' => nil,
'previous' => nil
}
parsed_response = Oj.load(last_response.body)
expect(last_response.status).to eq(200)
expect(parsed_response['resources'].pluck('guid')).to contain_exactly(package_in_org1.guid, package_in_org2.guid)
expect(parsed_response['pagination']).to eq(expected_pagination)
end
it 'filters by label selectors' do
target = VCAP::CloudController::PackageModel.make(app_guid: app_model.guid)
VCAP::CloudController::PackageModel.make(app_guid: app_model.guid)
VCAP::CloudController::PackageModel.make(app_guid: app_model.guid)
VCAP::CloudController::PackageLabelModel.make(key_name: 'fruit', value: 'strawberry', package: target)
get '/v3/packages?label_selector=fruit=strawberry', {}, user_header
expected_pagination = {
'total_results' => 1,
'total_pages' => 1,
'first' => { 'href' => "#{link_prefix}/v3/packages?label_selector=fruit%3Dstrawberry&page=1&per_page=50" },
'last' => { 'href' => "#{link_prefix}/v3/packages?label_selector=fruit%3Dstrawberry&page=1&per_page=50" },
'next' => nil,
'previous' => nil
}
parsed_response = Oj.load(last_response.body)
expect(last_response.status).to eq(200)
expect(parsed_response['resources'].count).to eq(1)
expect(parsed_response['resources'][0]['guid']).to eq(target.guid)
expect(parsed_response['pagination']).to eq(expected_pagination)
end
end
end
end
describe 'GET /v3/packages/:guid' do
let(:api_call) { ->(user_headers) { get "v3/packages/#{guid}", nil, user_headers } }
let(:package_model) do
VCAP::CloudController::PackageModel.make(app_guid: app_model.guid)
end
let(:guid) { package_model.guid }
let(:package_model_response_object) do
{
'type' => package_model.type,
'guid' => guid,
'data' => {
'checksum' => { 'type' => 'sha256', 'value' => anything },
'error' => nil
},
'state' => VCAP::CloudController::PackageModel::CREATED_STATE,
'relationships' => { 'app' => { 'data' => { 'guid' => app_model.guid } } },
'metadata' => { 'labels' => {}, 'annotations' => {} },
'created_at' => iso8601,
'updated_at' => iso8601,
'links' => {
'self' => { 'href' => "#{link_prefix}/v3/packages/#{guid}" },
'upload' => { 'href' => "#{link_prefix}/v3/packages/#{guid}/upload", 'method' => 'POST' },
'download' => { 'href' => "#{link_prefix}/v3/packages/#{guid}/download" },
'app' => { 'href' => "#{link_prefix}/v3/apps/#{app_model.guid}" }
}
}
end
let(:expected_codes_and_responses) do
h = Hash.new(code: 200, response_object: package_model_response_object)
h['org_auditor'] = { code: 404 }
h['org_billing_manager'] = { code: 404 }
h['no_role'] = { code: 404 }
h
end
it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
end
describe 'POST /v3/packages/:guid/upload' do
let(:type) { 'bits' }
let!(:package_model) do
VCAP::CloudController::PackageModel.make(app_guid: app_model.guid, type: type)
end
let(:space) { VCAP::CloudController::Space.make }
let(:app_model) { VCAP::CloudController::AppModel.make(guid: 'woof', space_guid: space.guid, name: 'meow') }
let(:guid) { package_model.guid }
let(:tmpdir) { Dir.mktmpdir }
let(:test_config_overrides) do
{ directories: { tmpdir: } }
end
let!(:tmpfile) do
File.open(File.join(tmpdir, 'application.zip'), 'w+') do |f|
f.write('application code')
f
end
end
let(:packages_params) do
{
bits_name: File.basename(tmpfile.path),
bits_path: tmpfile.path
}
end
before do
space.organization.add_user(user)
space.add_developer(user)
TestConfig.override(**test_config_overrides)
end
shared_examples 'upload bits successfully' do
it 'uploads the bits for the package' do
expect(Delayed::Job.count).to eq 0
post "/v3/packages/#{guid}/upload", packages_params.to_json, user_header
expect(Delayed::Job.count).to eq 1
expected_response = {
'type' => package_model.type,
'guid' => guid,
'data' => {
'checksum' => { 'type' => 'sha256', 'value' => anything },
'error' => nil
},
'state' => VCAP::CloudController::PackageModel::PENDING_STATE,
'relationships' => { 'app' => { 'data' => { 'guid' => app_model.guid } } },
'metadata' => { 'labels' => {}, 'annotations' => {} },
'created_at' => iso8601,
'updated_at' => iso8601,
'links' => {
'self' => { 'href' => "#{link_prefix}/v3/packages/#{guid}" },
'upload' => { 'href' => "#{link_prefix}/v3/packages/#{guid}/upload", 'method' => 'POST' },
'download' => { 'href' => "#{link_prefix}/v3/packages/#{guid}/download" },
'app' => { 'href' => "#{link_prefix}/v3/apps/#{app_model.guid}" }
}
}
parsed_response = Oj.load(last_response.body)
expect(last_response.status).to eq(200)
expect(parsed_response).to be_a_response_like(expected_response)
expected_metadata = { package_guid: package_model.guid }.to_json
event = VCAP::CloudController::Event.last
expect(event.values).to include({
type: 'audit.app.package.upload',
actor: user.guid,
actor_type: 'user',
actor_name: email,
actor_username: user_name,
actee: 'woof',
actee_type: 'app',
actee_name: 'meow',
metadata: expected_metadata,
space_guid: space.guid,
organization_guid: space.organization.guid
})
end
end
context 'with v2 resources' do
let(:packages_params) do
{
bits_name: 'application.zip',
bits_path: "#{tmpdir}/application.zip",
resources: '[{"fn":"path/to/content.txt","size":123,"sha1":"b907173290db6a155949ab4dc9b2d019dea0c901"},
{"fn":"path/to/code.jar","size":123,"sha1":"ff84f89760317996b9dd180ab996b079f418396f"},
{"fn":"path/to/code.jar","size":123,"sha1":"ff84f89760317996b9dd180ab996b079f418396f","mode":"644"}]'
}
end
include_examples 'upload bits successfully'
end
context 'with v3 resources' do
let(:packages_params) do
{
bits_name: 'application.zip',
bits_path: "#{tmpdir}/application.zip",
resources: '[{"path":"path/to/content.txt","size_in_bytes":123,"checksum": { "value" : "b907173290db6a155949ab4dc9b2d019dea0c901" }},
{"path":"path/to/code.jar","size_in_bytes":123,"checksum": { "value" : "ff84f89760317996b9dd180ab996b079f418396f" }},
{"path":"path/to/code.jar","size_in_bytes":123,"checksum": { "value" : "ff84f89760317996b9dd180ab996b079f418396f" },"mode":"644"}]'
}
end
include_examples 'upload bits successfully'
end
context 'telemetry' do
it 'logs the required fields when the package uploads' do
Timecop.freeze do
expected_json = {
'telemetry-source' => 'cloud_controller_ng',
'telemetry-time' => Time.now.to_datetime.rfc3339,
'upload-package' => {
'api-version' => 'v3',
'app-id' => OpenSSL::Digest::SHA256.hexdigest(app_model.guid),
'user-id' => OpenSSL::Digest::SHA256.hexdigest(user.guid)
}
}
expect_any_instance_of(ActiveSupport::Logger).to receive(:info).with(Oj.dump(expected_json))
post "/v3/packages/#{guid}/upload", packages_params.to_json, user_header
expect(last_response.status).to eq(200)
end
end
end
context 'permissions' do
before do
space.remove_developer(user)
space.organization.remove_user(user)
end
let(:api_call) { ->(user_headers) { post "/v3/packages/#{guid}/upload", packages_params.to_json, user_headers } }
let(:space) { VCAP::CloudController::Space.make }
let(:org) { space.organization }
let(:user) { VCAP::CloudController::User.make }
let(:package_model_response_object) do
{
'type' => package_model.type,
'guid' => guid,
'data' => {
'checksum' => { 'type' => 'sha256', 'value' => anything },
'error' => nil
},
'state' => VCAP::CloudController::PackageModel::PENDING_STATE,
'relationships' => { 'app' => { 'data' => { 'guid' => app_model.guid } } },
'metadata' => { 'labels' => {}, 'annotations' => {} },
'created_at' => iso8601,
'updated_at' => iso8601,
'links' => {
'self' => { 'href' => "#{link_prefix}/v3/packages/#{guid}" },
'upload' => { 'href' => "#{link_prefix}/v3/packages/#{guid}/upload", 'method' => 'POST' },
'download' => { 'href' => "#{link_prefix}/v3/packages/#{guid}/download" },
'app' => { 'href' => "#{link_prefix}/v3/apps/#{app_model.guid}" }
}
}
end
let(:expected_codes_and_responses) do
h = Hash.new(code: 200, response_object: package_model_response_object)
h['org_auditor'] = { code: 404 }
h['org_billing_manager'] = { code: 404 }
h['no_role'] = { code: 404 }
%w[admin_read_only global_auditor space_manager space_auditor org_manager space_supporter].each do |r|
h[r] = { code: 403, errors: CF_NOT_AUTHORIZED }
end
h
end
it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
context 'when organization is suspended' do
let(:expected_codes_and_responses) do
h = super()
h['space_developer'] = { code: 403, errors: CF_ORG_SUSPENDED }
h
end
before do
org.update(status: VCAP::CloudController::Organization::SUSPENDED)
end
it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
end
end
end
describe 'GET /v3/packages/:guid/download' do
let(:type) { 'bits' }
let!(:package_model) do
VCAP::CloudController::PackageModel.make(app_guid: app_model.guid, type: type)
end
let(:app_model) do
VCAP::CloudController::AppModel.make(guid: 'woof-guid', space_guid: space.guid, name: 'meow')
end
let(:space) { VCAP::CloudController::Space.make }
let(:bits_download_url) { CloudController::DependencyLocator.instance.blobstore_url_generator.package_download_url(package_model) }
let(:guid) { package_model.guid }
let(:temp_file) do
file = File.join(Dir.mktmpdir, 'application.zip')
TestZip.create(file, 1, 1024)
file
end
let(:upload_body) do
{
bits_name: 'application.zip',
bits_path: temp_file
}
end
before do
TestConfig.override(directories: { tmpdir: File.dirname(temp_file) }, kubernetes: {})
space.organization.add_user(user)
space.add_developer(user)
post "/v3/packages/#{guid}/upload", upload_body.to_json, user_header
Delayed::Worker.new.work_off
end
it 'downloads the bit(s) for a package' do
Timecop.freeze do
get "/v3/packages/#{guid}/download", {}, user_header
expect(last_response.status).to eq(302)
expect(last_response.headers['Location']).to eq(bits_download_url)
expected_metadata = { package_guid: package_model.guid }.to_json
event = VCAP::CloudController::Event.last
expect(event.values).to include({
type: 'audit.app.package.download',
actor: user.guid,
actor_type: 'user',
actor_name: email,
actor_username: user_name,
actee: 'woof-guid',
actee_type: 'app',
actee_name: 'meow',
metadata: expected_metadata,
space_guid: space.guid,
organization_guid: space.organization.guid
})
end
end
context 'permissions' do
before do
space.remove_developer(user)
space.organization.remove_user(user)
end
let(:api_call) { ->(user_headers) { get "/v3/packages/#{guid}/download", nil, user_headers } }
let(:space) { VCAP::CloudController::Space.make }
let(:org) { space.organization }
let(:user) { VCAP::CloudController::User.make }
let(:expected_codes_and_responses) do
h = Hash.new(code: 302)
h['global_auditor'] = { code: 403 }
h['org_auditor'] = { code: 404 }
h['org_billing_manager'] = { code: 404 }
h['org_manager'] = { code: 403 }
h['no_role'] = { code: 404 }
h['space_auditor'] = { code: 403 }
h['space_manager'] = { code: 403 }
h['space_supporter'] = { code: 403 }
h
end
it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
end
end
describe 'PATCH /v3/packages/:guid' do
let(:app_name) { 'sir meow' }
let(:app_guid) { 'meow-the-guid' }
let(:space) { VCAP::CloudController::Space.make }
let(:app_model) { VCAP::CloudController::AppModel.make(space_guid: space.guid, name: app_name, guid: app_guid) }
let!(:package_model) do
VCAP::CloudController::PackageModel.make(app_guid: app_model.guid)
end
let(:metadata) do
{
labels: {
release: 'stable',
'seriouseats.com/potato' => 'mashed'
},
annotations: { 'checksum' => 'SHA' }
}
end
let(:guid) { package_model.guid }
before do
space.organization.add_user user
space.add_developer user
end
it 'updates package metadata' do
patch "/v3/packages/#{guid}", { metadata: }.to_json, user_header
expected_metadata = {
'labels' => {
'release' => 'stable',
'seriouseats.com/potato' => 'mashed'
},
'annotations' => { 'checksum' => 'SHA' }
}
parsed_response = Oj.load(last_response.body)
expect(last_response.status).to eq(200)
expect(parsed_response['metadata']).to eq(expected_metadata)
end
context 'permissions' do
before do
space.remove_developer(user)
space.organization.remove_user(user)
end
let(:api_call) { ->(user_headers) { patch "/v3/packages/#{guid}", { metadata: }.to_json, user_headers } }
let(:space) { VCAP::CloudController::Space.make }
let(:org) { space.organization }
let(:user) { VCAP::CloudController::User.make }
let(:expected_codes_and_responses) do
h = Hash.new(code: 200)
h['org_auditor'] = { code: 404 }
h['org_billing_manager'] = { code: 404 }
h['no_role'] = { code: 404 }
%w[admin_read_only global_auditor space_manager space_auditor org_manager space_supporter].each do |r|
h[r] = { code: 403, errors: CF_NOT_AUTHORIZED }
end
h
end
it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
context 'when organization is suspended' do
let(:expected_codes_and_responses) do
h = super()
h['space_developer'] = { code: 403, errors: CF_ORG_SUSPENDED }
h
end
before do
org.update(status: VCAP::CloudController::Organization::SUSPENDED)
end
it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
end
end
end
describe 'DELETE /v3/packages/:guid' do
let(:app_name) { 'sir meow' }
let(:app_guid) { 'meow-the-guid' }
let(:space) { VCAP::CloudController::Space.make }
let(:app_model) { VCAP::CloudController::AppModel.make(space_guid: space.guid, name: app_name, guid: app_guid) }
let!(:package_model) do
VCAP::CloudController::PackageModel.make(app_guid: app_model.guid)
end
let(:guid) { package_model.guid }
before do
space.organization.add_user user
space.add_developer user
end
it 'deletes a package asynchronously' do
delete "/v3/packages/#{guid}", {}, user_header
expect(last_response.status).to eq(202)
expect(last_response.body).to eq('')
expect(last_response.header['Location']).to match(%r{jobs/[a-fA-F0-9-]+})
execute_all_jobs(expected_successes: 2, expected_failures: 0)
get "/v3/packages/#{guid}", {}, user_header
expect(last_response.status).to eq(404)
expected_metadata = { package_guid: guid }.to_json
event = VCAP::CloudController::Event.last
expect(event.values).to include({
type: 'audit.app.package.delete',
actor: user.guid,
actor_type: 'user',
actor_name: email,
actor_username: user_name,
actee: app_guid,
actee_type: 'app',
actee_name: app_name,
metadata: expected_metadata,
space_guid: space.guid,
organization_guid: space.organization.guid
})
end
context 'deleting metadata' do
it_behaves_like 'resource with metadata' do
let(:resource) { package_model }
let(:api_call) do
-> { delete "/v3/packages/#{resource.guid}", nil, user_header }
end
end
end
context 'permissions' do
before do
space.remove_developer(user)
space.organization.remove_user(user)
end
let(:api_call) { ->(user_headers) { delete "/v3/packages/#{guid}", nil, user_headers } }
let(:space) { VCAP::CloudController::Space.make }
let(:org) { space.organization }
let(:user) { VCAP::CloudController::User.make }
let(:expected_codes_and_responses) do
h = Hash.new(code: 202)
h['org_auditor'] = { code: 404 }
h['org_billing_manager'] = { code: 404 }
h['no_role'] = { code: 404 }
%w[admin_read_only global_auditor space_manager space_auditor org_manager space_supporter].each do |r|
h[r] = { code: 403, errors: CF_NOT_AUTHORIZED }
end
h
end
it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
context 'when organization is suspended' do
let(:expected_codes_and_responses) do
h = super()
h['space_developer'] = { code: 403, errors: CF_ORG_SUSPENDED }
h
end
before do
org.update(status: VCAP::CloudController::Organization::SUSPENDED)
end
it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
end
end
end
describe 'PATCH /internal/v4/packages/:guid' do
let!(:package_model) { VCAP::CloudController::PackageModel.make(state: VCAP::CloudController::PackageModel::PENDING_STATE) }
let(:body) do
{
'state' => 'READY',
'checksums' => [
{
'type' => 'sha1',
'value' => 'potato'
},
{
'type' => 'sha256',
'value' => 'potatoest'
}
]
}.to_json
end
let(:guid) { package_model.guid }
it 'updates a package' do
patch "/internal/v4/packages/#{guid}", body
expect(last_response.status).to eq(204)
expect(last_response.body).to eq('')
end
end
end