spec/request/revisions_spec.rb
require 'spec_helper'
require 'request_spec_shared_examples'
RSpec.describe 'Revisions' do
let(:user) { VCAP::CloudController::User.make }
let(:user_header) { headers_for(user, email: user_email, user_name: user_name) }
let(:org) { VCAP::CloudController::Organization.make(created_at: 3.days.ago) }
let(:space) { VCAP::CloudController::Space.make(organization: org) }
let(:stack) { VCAP::CloudController::Stack.make }
let(:user_email) { Sham.email }
let(:user_name) { 'some-username' }
let(:app_model) { VCAP::CloudController::AppModel.make(name: 'app_name', space: space) }
let!(:revision) { VCAP::CloudController::RevisionModel.make(app: app_model, version: 42) }
describe 'GET /v3/revisions/:revguid' do
let(:droplet) { VCAP::CloudController::DropletModel.make(process_types: { 'web' => 'bake rackup' }) }
let(:revision) { VCAP::CloudController::RevisionModel.make(app: app_model, version: 42, droplet: droplet) }
let!(:revision_worker_process_command) { VCAP::CloudController::RevisionProcessCommandModel.make(revision: revision, process_type: 'worker', process_command: './work') }
let!(:revision_sidecar) { VCAP::CloudController::RevisionSidecarModel.make(revision: revision, name: 'my-sidecar', command: 'run-sidecar', memory: 300) }
let(:api_call) { ->(user_headers) { get "/v3/revisions/#{revision.guid}", nil, user_headers } }
let(:revision_model_response_object) do
{
'guid' => revision.guid,
'version' => revision.version,
'description' => revision.description,
'droplet' => {
'guid' => revision.droplet_guid
},
'relationships' => {
'app' => {
'data' => {
'guid' => app_model.guid
}
}
},
'created_at' => iso8601,
'updated_at' => iso8601,
'links' => {
'self' => {
'href' => "#{link_prefix}/v3/revisions/#{revision.guid}"
},
'app' => {
'href' => "#{link_prefix}/v3/apps/#{app_model.guid}"
},
'environment_variables' => {
'href' => "#{link_prefix}/v3/revisions/#{revision.guid}/environment_variables"
}
},
'metadata' => { 'labels' => {}, 'annotations' => {} },
'processes' => {
'web' => {
'command' => nil
},
'worker' => {
'command' => './work'
}
},
'sidecars' => [{
'name' => 'my-sidecar',
'command' => 'run-sidecar',
'process_types' => ['web'],
'memory_in_mb' => 300
}],
'deployable' => true
}
end
let(:expected_codes_and_responses) do
h = Hash.new(code: 200, response_object: revision_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 'GET /v3/apps/:guid/revisions' do
let!(:revision2) { VCAP::CloudController::RevisionModel.make(app: app_model, version: 43, description: 'New droplet deployed') }
context 'gets all revisions for an app' do
it_behaves_like 'permissions for list endpoint', ALL_PERMISSIONS do
let(:api_call) { ->(user_headers) { get "/v3/apps/#{app_model.guid}/revisions", { per_page: '2' }, user_headers } }
let(:revision_response_objects) do
[
{
'guid' => revision.guid,
'version' => revision.version,
'description' => revision.description,
'droplet' => {
'guid' => revision.droplet_guid
},
'relationships' => {
'app' => {
'data' => {
'guid' => app_model.guid
}
}
},
'created_at' => iso8601,
'updated_at' => iso8601,
'links' => {
'self' => {
'href' => "#{link_prefix}/v3/revisions/#{revision.guid}"
},
'app' => {
'href' => "#{link_prefix}/v3/apps/#{app_model.guid}"
},
'environment_variables' => {
'href' => "#{link_prefix}/v3/revisions/#{revision.guid}/environment_variables"
}
},
'metadata' => { 'labels' => {}, 'annotations' => {} },
'processes' => {
'web' => {
'command' => nil
}
},
'sidecars' => [],
'deployable' => true
},
{
'guid' => revision2.guid,
'version' => revision2.version,
'description' => revision2.description,
'droplet' => {
'guid' => revision2.droplet_guid
},
'relationships' => {
'app' => {
'data' => {
'guid' => app_model.guid
}
}
},
'created_at' => iso8601,
'updated_at' => iso8601,
'links' => {
'self' => {
'href' => "#{link_prefix}/v3/revisions/#{revision2.guid}"
},
'app' => {
'href' => "#{link_prefix}/v3/apps/#{app_model.guid}"
},
'environment_variables' => {
'href' => "#{link_prefix}/v3/revisions/#{revision2.guid}/environment_variables"
}
},
'metadata' => { 'labels' => {}, 'annotations' => {} },
'processes' => {
'web' => {
'command' => nil
}
},
'sidecars' => [],
'deployable' => true
}
]
end
let(:message) { VCAP::CloudController::AppRevisionsListMessage }
let(:expected_codes_and_responses) do
h = Hash.new(code: 200, response_objects: revision_response_objects)
h['org_auditor'] = { code: 404 }
h['org_billing_manager'] = { code: 404 }
h['no_role'] = { code: 404 }
h
end
end
context 'when using all params' do
before do
space.organization.add_user(user)
space.add_developer(user)
end
it_behaves_like 'list query endpoint' do
let(:message) { VCAP::CloudController::AppRevisionsListMessage }
let(:request) { "/v3/apps/#{app_model.guid}/revisions" }
let(:excluded_params) { [:deployable] }
let(:params) do
{
page: '2',
per_page: '10',
order_by: 'updated_at',
guids: app_model.guid.to_s,
versions: '1,2',
label_selector: 'foo,bar',
created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}",
updated_ats: { gt: Time.now.utc.iso8601 }
}
end
end
end
end
context 'gets a subset of revisions for an app' do
before do
space.organization.add_user(user)
space.add_developer(user)
end
context 'filtering' do
it 'gets a list of revisions matching the provided versions' do
revision3 = VCAP::CloudController::RevisionModel.make(app: app_model, version: 44, description: 'Rollback to revision 42')
get "/v3/apps/#{app_model.guid}/revisions?per_page=2&versions=42,44", nil, user_header
expect(last_response.status).to eq(200)
parsed_response = Oj.load(last_response.body)
expect(parsed_response).to be_a_response_like(
{
'pagination' => {
'total_results' => 2,
'total_pages' => 1,
'first' => {
'href' => "#{link_prefix}/v3/apps/#{app_model.guid}/revisions?page=1&per_page=2&versions=42%2C44"
},
'last' => {
'href' => "#{link_prefix}/v3/apps/#{app_model.guid}/revisions?page=1&per_page=2&versions=42%2C44"
},
'next' => nil,
'previous' => nil
},
'resources' => [
{
'guid' => revision.guid,
'version' => revision.version,
'relationships' => {
'app' => {
'data' => {
'guid' => app_model.guid
}
}
},
'description' => revision.description,
'droplet' => {
'guid' => revision.droplet_guid
},
'created_at' => iso8601,
'updated_at' => iso8601,
'links' => {
'self' => {
'href' => "#{link_prefix}/v3/revisions/#{revision.guid}"
},
'app' => {
'href' => "#{link_prefix}/v3/apps/#{app_model.guid}"
},
'environment_variables' => {
'href' => "#{link_prefix}/v3/revisions/#{revision.guid}/environment_variables"
}
},
'metadata' => { 'labels' => {}, 'annotations' => {} },
'processes' => {
'web' => {
'command' => nil
}
},
'sidecars' => [],
'deployable' => true
},
{
'guid' => revision3.guid,
'version' => revision3.version,
'description' => revision3.description,
'droplet' => {
'guid' => revision3.droplet_guid
},
'relationships' => {
'app' => {
'data' => {
'guid' => app_model.guid
}
}
},
'created_at' => iso8601,
'updated_at' => iso8601,
'links' => {
'self' => {
'href' => "#{link_prefix}/v3/revisions/#{revision3.guid}"
},
'app' => {
'href' => "#{link_prefix}/v3/apps/#{app_model.guid}"
},
'environment_variables' => {
'href' => "#{link_prefix}/v3/revisions/#{revision3.guid}/environment_variables"
}
},
'metadata' => { 'labels' => {}, 'annotations' => {} },
'processes' => {
'web' => {
'command' => nil
}
},
'sidecars' => [],
'deployable' => true
}
]
}
)
end
context 'label_selector' do
let!(:revisionA) { VCAP::CloudController::RevisionModel.make(app: app_model) }
let!(:revisionB) { VCAP::CloudController::RevisionModel.make(app: app_model) }
let!(:revisionC) { VCAP::CloudController::RevisionModel.make(app: app_model) }
let!(:revAFruit) { VCAP::CloudController::RevisionLabelModel.make(key_name: 'fruit', value: 'strawberry', resource_guid: revisionA.guid) }
let!(:revAAnimal) { VCAP::CloudController::RevisionLabelModel.make(key_name: 'animal', value: 'horse', resource_guid: revisionA.guid) }
let!(:revBEnv) { VCAP::CloudController::RevisionLabelModel.make(key_name: 'env', value: 'prod', resource_guid: revisionB.guid) }
let!(:revBAnimal) { VCAP::CloudController::RevisionLabelModel.make(key_name: 'animal', value: 'dog', resource_guid: revisionB.guid) }
let!(:revCEnv) { VCAP::CloudController::RevisionLabelModel.make(key_name: 'env', value: 'prod', resource_guid: revisionC.guid) }
let!(:revCAnimal) { VCAP::CloudController::RevisionLabelModel.make(key_name: 'animal', value: 'horse', resource_guid: revisionC.guid) }
it 'returns the matching revisions' do
get "/v3/apps/#{app_model.guid}/revisions?label_selector=!fruit,env=prod,animal in (dog,horse)", nil, user_header
expect(last_response.status).to eq(200)
parsed_response = Oj.load(last_response.body)
expect(parsed_response['resources'].pluck('guid')).to contain_exactly(revisionB.guid, revisionC.guid)
end
end
context 'filtering by guids' do
before do
VCAP::CloudController::RevisionModel.plugin :timestamps, update_on_create: false
end
let(:droplet) { VCAP::CloudController::DropletModel.make(app: app_model) }
# .make updates the resource after creating it, over writing our passed in updated_at timestamp
# Therefore we cannot use shared_examples as the updated_at will not be as written
let!(:resource_1) do
VCAP::CloudController::RevisionModel.create(
created_at: '2020-05-26T18:47:01Z',
updated_at: '2020-05-26T18:47:01Z',
droplet: droplet,
description: '',
app: app_model
)
end
let!(:resource_2) do
VCAP::CloudController::RevisionModel.create(
created_at: '2020-05-26T18:47:02Z',
updated_at: '2020-05-26T18:47:02Z',
droplet: droplet,
description: '',
app: app_model
)
end
let!(:resource_3) do
VCAP::CloudController::RevisionModel.create(
created_at: '2020-05-26T18:47:03Z',
updated_at: '2020-05-26T18:47:03Z',
droplet: droplet,
description: '',
app: app_model
)
end
after do
VCAP::CloudController::RevisionModel.plugin :timestamps, update_on_create: true
end
it 'filters by the created at' do
get "/v3/apps/#{app_model.guid}/revisions?guids=#{resource_1.guid},#{resource_2.guid}", nil, admin_headers
expect(last_response).to have_status_code(200)
expect(parsed_response['resources'].pluck('guid')).to contain_exactly(resource_1.guid, resource_2.guid)
end
end
context 'filtering by timestamps' do
before do
VCAP::CloudController::RevisionModel.plugin :timestamps, update_on_create: false
end
let(:droplet) { VCAP::CloudController::DropletModel.make(app: app_model) }
# .make updates the resource after creating it, over writing our passed in updated_at timestamp
# Therefore we cannot use shared_examples as the updated_at will not be as written
let!(:resource_1) do
VCAP::CloudController::RevisionModel.create(
created_at: '2020-05-26T18:47:01Z',
updated_at: '2020-05-26T18:47:01Z',
droplet: droplet,
description: '',
app: app_model
)
end
let!(:resource_2) do
VCAP::CloudController::RevisionModel.create(
created_at: '2020-05-26T18:47:02Z',
updated_at: '2020-05-26T18:47:02Z',
droplet: droplet,
description: '',
app: app_model
)
end
let!(:resource_3) do
VCAP::CloudController::RevisionModel.create(
created_at: '2020-05-26T18:47:03Z',
updated_at: '2020-05-26T18:47:03Z',
droplet: droplet,
description: '',
app: app_model
)
end
let!(:resource_4) do
VCAP::CloudController::RevisionModel.create(
created_at: '2020-05-26T18:47:04Z',
updated_at: '2020-05-26T18:47:04Z',
droplet: droplet,
description: '',
app: app_model
)
end
after do
VCAP::CloudController::RevisionModel.plugin :timestamps, update_on_create: true
end
it 'filters by the created at' do
get "/v3/apps/#{app_model.guid}/revisions?created_ats[lt]=#{resource_3.created_at.iso8601}", nil, admin_headers
expect(last_response).to have_status_code(200)
expect(parsed_response['resources'].pluck('guid')).to contain_exactly(resource_1.guid, resource_2.guid)
end
it 'filters by the updated_at' do
get "/v3/apps/#{app_model.guid}/revisions?updated_ats[lt]=#{resource_3.updated_at.iso8601}", nil, admin_headers
expect(last_response).to have_status_code(200)
expect(parsed_response['resources'].pluck('guid')).to contain_exactly(resource_1.guid, resource_2.guid)
end
end
end
end
end
describe 'PATCH /v3/revisions/:revguid' do
before do
space.organization.add_user(user)
space.add_developer(user)
end
let(:update_request) do
{
metadata: {
labels: {
freaky: 'thursday'
},
annotations: {
quality: 'p sus'
}
}
}.to_json
end
it 'updates the revision with metadata' do
patch "/v3/revisions/#{revision.guid}", update_request, user_header
expect(last_response.status).to eq(200)
parsed_response = Oj.load(last_response.body)
expect(parsed_response).to be_a_response_like(
{
'guid' => revision.guid,
'version' => revision.version,
'description' => revision.description,
'droplet' => {
'guid' => revision.droplet_guid
},
'relationships' => {
'app' => {
'data' => {
'guid' => app_model.guid
}
}
},
'created_at' => iso8601,
'updated_at' => iso8601,
'links' => {
'self' => {
'href' => "#{link_prefix}/v3/revisions/#{revision.guid}"
},
'app' => {
'href' => "#{link_prefix}/v3/apps/#{app_model.guid}"
},
'environment_variables' => {
'href' => "#{link_prefix}/v3/revisions/#{revision.guid}/environment_variables"
}
},
'metadata' => {
'labels' => { 'freaky' => 'thursday' },
'annotations' => { 'quality' => 'p sus' }
},
'processes' => {
'web' => {
'command' => nil
}
},
'sidecars' => [],
'deployable' => true
}
)
end
context 'permissions' do
let(:api_call) { ->(user_headers) { patch "/v3/revisions/#{revision.guid}", update_request, user_headers } }
let(:expected_codes_and_responses) do
h = Hash.new(code: 403, errors: CF_NOT_AUTHORIZED)
%w[no_role org_auditor org_billing_manager].each { |r| h[r] = { code: 404 } }
%w[admin space_developer].each { |r| h[r] = { code: 200 } }
h
end
before do
space.remove_developer(user)
space.organization.remove_user(user)
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/revision/:revguid/environment_variables' do
before do
space.organization.add_user(user)
space.add_developer(user)
end
let!(:revision2) do
VCAP::CloudController::RevisionModel.make(
app: app_model,
version: 43,
environment_variables: { 'key' => 'value' }
)
end
it 'gets the environment variables for the revision' do
get "/v3/revisions/#{revision2.guid}/environment_variables", nil, user_header
expect(last_response.status).to eq(200), last_response.body
parsed_response = Oj.load(last_response.body)
expect(parsed_response).to be_a_response_like(
{
'var' => {
'key' => 'value'
},
'links' => {
'self' => { 'href' => "#{link_prefix}/v3/revisions/#{revision2.guid}/environment_variables" },
'revision' => { 'href' => "#{link_prefix}/v3/revisions/#{revision2.guid}" },
'app' => { 'href' => "#{link_prefix}/v3/apps/#{app_model.guid}" }
}
}
)
end
end
describe 'GET /v3/apps/:guid/revisions/deployed' do
let!(:revision2) { VCAP::CloudController::RevisionModel.make(app: app_model, version: 43, description: 'New droplet deployed') }
let!(:revision3) { VCAP::CloudController::RevisionModel.make(app: app_model, version: 44, description: 'New environment variables') }
let!(:process) { VCAP::CloudController::ProcessModel.make(app: app_model, revision: revision, type: 'web', state: 'STARTED') }
let!(:process2) { VCAP::CloudController::ProcessModel.make(app: app_model, revision: revision2, type: 'worker', state: 'STARTED') }
let!(:process3) { VCAP::CloudController::ProcessModel.make(app: app_model, revision: revision3, type: 'web', state: 'STOPPED') }
it_behaves_like 'permissions for list endpoint', ALL_PERMISSIONS do
let(:api_call) { ->(user_headers) { get "/v3/apps/#{app_model.guid}/revisions/deployed?per_page=2", nil, user_headers } }
let(:revision_response_objects) do
[
{
'guid' => revision.guid,
'version' => revision.version,
'description' => revision.description,
'droplet' => {
'guid' => revision.droplet_guid
},
'relationships' => {
'app' => {
'data' => {
'guid' => app_model.guid
}
}
},
'created_at' => iso8601,
'updated_at' => iso8601,
'links' => {
'self' => {
'href' => "#{link_prefix}/v3/revisions/#{revision.guid}"
},
'app' => {
'href' => "#{link_prefix}/v3/apps/#{app_model.guid}"
},
'environment_variables' => {
'href' => "#{link_prefix}/v3/revisions/#{revision.guid}/environment_variables"
}
},
'metadata' => { 'labels' => {}, 'annotations' => {} },
'processes' => {
'web' => {
'command' => nil
}
},
'sidecars' => [],
'deployable' => true
},
{
'guid' => revision2.guid,
'version' => revision2.version,
'description' => revision2.description,
'droplet' => {
'guid' => revision2.droplet_guid
},
'relationships' => {
'app' => {
'data' => {
'guid' => app_model.guid
}
}
},
'created_at' => iso8601,
'updated_at' => iso8601,
'links' => {
'self' => {
'href' => "#{link_prefix}/v3/revisions/#{revision2.guid}"
},
'app' => {
'href' => "#{link_prefix}/v3/apps/#{app_model.guid}"
},
'environment_variables' => {
'href' => "#{link_prefix}/v3/revisions/#{revision2.guid}/environment_variables"
}
},
'metadata' => { 'labels' => {}, 'annotations' => {} },
'processes' => {
'web' => {
'command' => nil
}
},
'sidecars' => [],
'deployable' => true
}
]
end
let(:expected_codes_and_responses) do
h = Hash.new(code: 200, response_objects: revision_response_objects)
h['org_auditor'] = { code: 404 }
h['org_billing_manager'] = { code: 404 }
h['no_role'] = { code: 404 }
h
end
end
end
end