spec/unit/repositories/app_usage_event_repository_spec.rb
require 'spec_helper'
require 'repositories/app_usage_event_repository'
module VCAP::CloudController
module Repositories
RSpec.describe AppUsageEventRepository do
subject(:repository) { AppUsageEventRepository.new }
describe '#find' do
context 'when the event exists' do
let(:event) { AppUsageEvent.make }
it 'returns the event' do
expect(repository.find(event.guid)).to eq(event)
end
end
context 'when the event does not exist' do
it 'returns nil' do
expect(repository.find('does-not-exist')).to be_nil
end
end
end
describe '#create_from_process' do
let(:parent_app) { AppModel.make(name: 'parent-app') }
let(:process) { ProcessModelFactory.make(app: parent_app, type: 'other') }
it 'creates an event which matches the app' do
event = repository.create_from_process(process)
expect(event).to match_app(process)
expect(event.parent_app_name).to eq('parent-app')
expect(event.parent_app_guid).to eq(parent_app.guid)
expect(event.process_type).to eq('other')
end
it 'creates an event with default previous attributes' do
event = repository.create_from_process(process)
default_instances = ProcessModel.db_schema[:instances][:default].to_i
default_memory = VCAP::CloudController::Config.config.get(:default_app_memory)
expect(event.previous_state).to eq('STOPPED')
expect(event.previous_instance_count).to eq(default_instances)
expect(event.previous_memory_in_mb_per_instance).to eq(default_memory)
end
context 'when a custom state is provided' do
let(:custom_state) { 'CUSTOM' }
it 'populates the event with the custom state' do
event = repository.create_from_process(process, custom_state)
expect(event.state).to eq(custom_state)
event.state = process.state
expect(event).to match_app(process)
end
end
context 'when the app is created' do
context 'when the package is pending' do
before do
process.desired_droplet.destroy
process.reload
end
it 'creates an event with pending package state' do
event = repository.create_from_process(process)
expect(event).to match_app(process)
end
end
context 'when the package is staged' do
it 'creates an event with staged package state' do
event = repository.create_from_process(process)
expect(event).to match_app(process)
end
end
context 'when the package is failed' do
before do
process.desired_droplet.update(state: DropletModel::FAILED_STATE)
process.reload
end
it 'creates an event with failed package state' do
event = repository.create_from_process(process)
expect(event).to match_app(process)
end
end
end
context 'when an admin buildpack is associated with the app' do
before do
process.desired_droplet.update(
buildpack_receipt_buildpack_guid: 'buildpack-guid',
buildpack_receipt_buildpack: 'buildpack-name'
)
end
it 'creates an event that contains the detected buildpack guid and name' do
event = repository.create_from_process(process)
expect(event).to match_app(process)
expect(event.buildpack_guid).to eq('buildpack-guid')
expect(event.buildpack_name).to eq('buildpack-name')
end
end
context 'when a custom buildpack is associated with the app' do
let(:buildpack_url) { 'https://git.example.com/repo.git' }
before do
process.app.lifecycle_data.update(buildpacks: [buildpack_url])
end
it 'creates an event with the buildpack url as the name' do
event = repository.create_from_process(process)
expect(event.buildpack_name).to eq('https://git.example.com/repo.git')
end
context 'where there are user credentials in the buildpack url' do
let(:buildpack_url) { 'https://super:secret@git.example.com/repo.git' }
it 'redacts them' do
event = repository.create_from_process(process)
expect(event.buildpack_name).to eq('https://***:***@git.example.com/repo.git')
end
end
it 'creates an event without a buildpack guid' do
event = repository.create_from_process(process)
expect(event.buildpack_guid).to be_nil
end
end
context "when the DEA doesn't provide optional buildpack information" do
before do
process.app.lifecycle_data.update(buildpacks: nil)
end
it 'creates an event that does not contain buildpack name or guid' do
event = repository.create_from_process(process)
expect(event.buildpack_guid).to be_nil
expect(event.buildpack_name).to be_nil
end
end
context 'fails to create the event' do
before do
process.state = nil
end
it 'raises an error' do
expect do
repository.create_from_process(process)
end.to raise_error(Sequel::NotNullConstraintViolation)
end
end
context 'when the app already existed' do
let(:old_state) { 'STARTED' }
let(:old_instances) { 4 }
let(:old_memory) { 256 }
let(:process) { ProcessModelFactory.make(state: old_state, instances: old_instances, memory: old_memory) }
it 'always sets previous_package_state to UNKNOWN' do
event = repository.create_from_process(process)
expect(event.previous_package_state).to eq('UNKNOWN')
end
context 'when the same attribute values are set' do
before do
process.state = old_state
process.instances = old_instances
process.memory = old_memory
end
it 'creates event with previous attributes' do
event = repository.create_from_process(process)
expect(event.previous_state).to eq(old_state)
expect(event.previous_instance_count).to eq(old_instances)
expect(event.previous_memory_in_mb_per_instance).to eq(old_memory)
end
end
context 'when app attributes change' do
let(:new_state) { 'STOPPED' }
let(:new_instances) { 2 }
let(:new_memory) { 1024 }
before do
process.state = new_state
process.instances = new_instances
process.memory = new_memory
end
it 'stores new values' do
event = repository.create_from_process(process)
expect(event.state).to eq(new_state)
expect(event.instance_count).to eq(new_instances)
expect(event.memory_in_mb_per_instance).to eq(new_memory)
end
it 'stores previous values' do
event = repository.create_from_process(process)
expect(event.previous_state).to eq(old_state)
expect(event.previous_instance_count).to eq(old_instances)
expect(event.previous_memory_in_mb_per_instance).to eq(old_memory)
end
end
end
end
describe '#create_from_task' do
let!(:task) { TaskModel.make(memory_in_mb: 222) }
let(:state) { 'TEST_STATE' }
it 'creates an AppUsageEvent' do
expect do
repository.create_from_task(task, state)
end.to change(AppUsageEvent, :count).by(1)
end
describe 'the created event' do
it 'sets the state to what is passed in' do
event = repository.create_from_task(task, state)
expect(event.state).to eq('TEST_STATE')
end
it 'sets the attributes based on the task' do
event = repository.create_from_task(task, state)
expect(event.memory_in_mb_per_instance).to eq(222)
expect(event.previous_memory_in_mb_per_instance).to eq(222)
expect(event.instance_count).to eq(1)
expect(event.previous_instance_count).to eq(1)
expect(event.app_guid).to eq('')
expect(event.app_name).to eq('')
expect(event.space_guid).to eq(task.space.guid)
expect(event.space_guid).to be_present
expect(event.space_name).to eq(task.space.name)
expect(event.space_name).to be_present
expect(event.org_guid).to eq(task.space.organization.guid)
expect(event.org_guid).to be_present
expect(event.buildpack_guid).to be_nil
expect(event.buildpack_name).to be_nil
expect(event.previous_state).to eq('RUNNING')
expect(event.package_state).to eq('STAGED')
expect(event.previous_package_state).to eq('STAGED')
expect(event.parent_app_guid).to eq(task.app.guid)
expect(event.parent_app_guid).to be_present
expect(event.parent_app_name).to eq(task.app.name)
expect(event.parent_app_name).to be_present
expect(event.process_type).to be_nil
expect(event.task_guid).to eq(task.guid)
expect(event.task_name).to eq(task.name)
end
end
context 'when the task exists' do
let(:old_state) { TaskModel::RUNNING_STATE }
let(:old_memory) { 256 }
let(:existing_task) { TaskModel.make(state: old_state, memory_in_mb: old_memory) }
context 'when the same attribute values are set' do
before do
existing_task.memory_in_mb = old_memory
end
it 'creates event with previous attributes' do
event = repository.create_from_task(existing_task, state)
expect(event.previous_state).to eq(old_state)
expect(event.previous_package_state).to eq('STAGED')
expect(event.previous_instance_count).to eq(1)
expect(event.previous_memory_in_mb_per_instance).to eq(old_memory)
end
end
context 'when task attributes change' do
let(:new_state) { TaskModel::FAILED_STATE }
let(:new_memory) { 1024 }
before do
existing_task.memory_in_mb = new_memory
end
it 'stores new values' do
event = repository.create_from_task(existing_task, new_state)
expect(event.state).to eq(new_state)
expect(event.memory_in_mb_per_instance).to eq(new_memory)
end
it 'stores previous values' do
event = repository.create_from_task(existing_task, state)
expect(event.previous_state).to eq(old_state)
expect(event.previous_package_state).to eq('STAGED')
expect(event.previous_instance_count).to eq(1)
expect(event.previous_memory_in_mb_per_instance).to eq(old_memory)
end
end
end
end
describe '#create_from_build' do
let(:org) { Organization.make(guid: 'org-1') }
let(:space) { Space.make(guid: 'space-1', name: 'space-name', organization: org) }
let(:app_model) { AppModel.make(guid: 'app-1', name: 'frank-app', space: space) }
let(:package_state) { PackageModel::READY_STATE }
let(:package) { PackageModel.make(guid: 'package-1', app_guid: app_model.guid, state: package_state) }
let!(:build) { BuildModel.make(guid: 'build-1', package: package, app_guid: app_model.guid, state: BuildModel::STAGING_STATE) }
let(:state) { 'TEST_STATE' }
it 'creates an AppUsageEvent' do
expect do
repository.create_from_build(build, state)
end.to change(AppUsageEvent, :count).by(1)
end
describe 'the created event' do
it 'sets the state to what is passed in' do
event = repository.create_from_build(build, state)
expect(event.state).to eq('TEST_STATE')
end
it 'sets the attributes based on the build' do
build.update(
droplet: DropletModel.make(buildpack_receipt_buildpack: 'le-buildpack'),
buildpack_lifecycle_data: BuildpackLifecycleDataModel.make
)
event = repository.create_from_build(build, state)
expect(event.state).to eq('TEST_STATE')
expect(event.previous_state).to eq('STAGING')
expect(event.instance_count).to eq(1)
expect(event.previous_instance_count).to eq(1)
expect(event.memory_in_mb_per_instance).to eq(1024)
expect(event.previous_memory_in_mb_per_instance).to eq(1024)
expect(event.org_guid).to eq('org-1')
expect(event.space_guid).to eq('space-1')
expect(event.space_name).to eq('space-name')
expect(event.parent_app_guid).to eq('app-1')
expect(event.parent_app_name).to eq('frank-app')
expect(event.package_guid).to eq('package-1')
expect(event.app_guid).to eq('')
expect(event.app_name).to eq('')
expect(event.process_type).to be_nil
expect(event.buildpack_name).to eq('le-buildpack')
expect(event.buildpack_guid).to be_nil
expect(event.package_state).to eq(package_state)
expect(event.previous_package_state).to eq(package_state)
expect(event.task_guid).to be_nil
expect(event.task_name).to be_nil
end
end
context 'buildpack builds' do
context 'when the build does NOT have an associated droplet but does have lifecycle data' do
before do
build.update(
buildpack_lifecycle_data: BuildpackLifecycleDataModel.make(buildpacks: ['http://git.url.example.com'])
)
end
it 'sets the event buildpack_name to the lifecycle data buildpack' do
event = repository.create_from_build(build, state)
expect(event.buildpack_name).to eq('http://git.url.example.com')
expect(event.buildpack_guid).to be_nil
end
context 'when buildpack lifecycle info contains credentials in buildpack url' do
before do
build.update(
buildpack_lifecycle_data: BuildpackLifecycleDataModel.make(buildpacks: ['http://ping:pong@example.com'])
)
end
it 'redacts credentials from the url' do
event = repository.create_from_build(build, state)
expect(event.buildpack_name).to eq('http://***:***@example.com')
expect(event.buildpack_guid).to be_nil
end
end
end
context 'when the build has BOTH an associated droplet and lifecycle data' do
let!(:build) do
BuildModel.make(
:buildpack,
guid: 'build-1',
package_guid: package.guid,
app_guid: app_model.guid
)
end
let!(:droplet) do
DropletModel.make(
:buildpack,
buildpack_receipt_buildpack: 'a-buildpack',
buildpack_receipt_buildpack_guid: 'a-buildpack-guid',
build: build
)
end
before do
Buildpack.make(name: 'ruby_buildpack')
build.update(
buildpack_lifecycle_data: BuildpackLifecycleDataModel.make(buildpacks: ['ruby_buildpack'])
)
end
it 'prefers the buildpack receipt info' do
event = repository.create_from_build(build, state)
expect(event.buildpack_name).to eq('a-buildpack')
expect(event.buildpack_guid).to eq('a-buildpack-guid')
end
end
end
context 'docker builds' do
let!(:build) do
BuildModel.make(
:docker,
guid: 'build-1',
package_guid: package.guid,
app_guid: app_model.guid
)
end
it 'does not include buildpack_guid or buildpack_name' do
event = repository.create_from_build(build, state)
expect(event.buildpack_name).to be_nil
expect(event.buildpack_guid).to be_nil
end
end
context 'when the build is updating its state' do
let(:old_build_state) { BuildModel::STAGED_STATE }
let(:existing_build) do
BuildModel.make(
guid: 'existing-build',
state: old_build_state,
package: package,
app_guid: app_model.guid
)
end
context 'when the same attribute values are set' do
before do
existing_build.state = old_build_state
end
it 'creates event with previous attributes' do
event = repository.create_from_build(existing_build, state)
expect(event.previous_state).to eq(old_build_state)
expect(event.previous_package_state).to eq(package_state)
expect(event.previous_instance_count).to eq(1)
end
end
context 'when package attributes change' do
let(:new_state) { BuildModel::STAGED_STATE }
let(:new_package_state) { PackageModel::FAILED_STATE }
let(:new_memory) { 1024 }
before do
existing_build.package.state = new_package_state
end
it 'stores new values' do
event = repository.create_from_build(existing_build, new_state)
expect(event.state).to eq(new_state)
expect(event.package_state).to eq(new_package_state)
expect(event.instance_count).to eq(1)
end
it 'stores previous values' do
event = repository.create_from_build(existing_build, new_state)
expect(event.previous_state).to eq(old_build_state)
expect(event.previous_package_state).to eq(package_state)
expect(event.previous_instance_count).to eq(1)
end
end
context 'when the build has no package' do
let(:existing_build) { BuildModel.make(guid: 'existing-build', state: old_build_state, app_guid: app_model.guid) }
context 'when an attribute changes' do
before do
existing_build.state = BuildModel::STAGED_STATE
end
it 'returns no previous package state' do
event = repository.create_from_build(existing_build, state)
expect(event.previous_package_state).to be_nil
end
end
end
end
end
describe '#purge_and_reseed_started_apps!', isolation: :truncation do
let(:process) { ProcessModelFactory.make }
before do
allow(ProcessObserver).to receive(:updated)
end
it 'purges all existing events' do
3.times { repository.create_from_process(process) }
expect do
repository.purge_and_reseed_started_apps!
end.to change(AppUsageEvent, :count).to(0)
end
context 'when there are started apps' do
before do
process.state = 'STARTED'
process.save
ProcessModelFactory.make(state: 'STOPPED')
end
it 'creates new events for the started apps' do
process.state = 'STOPPED'
repository.create_from_process(process)
process.state = 'STARTED'
repository.create_from_process(process)
started_app_count = ProcessModel.where(state: 'STARTED').count
expect(AppUsageEvent.count > 1).to be true
expect do
repository.purge_and_reseed_started_apps!
end.to change(AppUsageEvent, :count).to(started_app_count)
expect(AppUsageEvent.last).to match_app(process)
end
it 'sets the parent_app_name and parent_app_guid' do
repository.purge_and_reseed_started_apps!
expect(AppUsageEvent.last.parent_app_name).to eq(process.app.name)
expect(AppUsageEvent.last.parent_app_guid).to eq(process.app.guid)
end
context 'with associated buildpack information' do
before do
process.desired_droplet.update(
buildpack_receipt_buildpack: 'detected-name',
buildpack_receipt_buildpack_guid: 'detected-guid'
)
process.reload
end
it 'preserves the buildpack info in the new event' do
repository.purge_and_reseed_started_apps!
event = AppUsageEvent.last
expect(event).to match_app(process)
expect(event.buildpack_name).to eq('detected-name')
expect(event.buildpack_guid).to eq('detected-guid')
end
end
describe 'package_state' do
context 'when the latest_droplet is STAGED' do
context 'and there is no current_droplet' do
before do
process.app.update(droplet: nil)
process.reload
end
it 'is PENDING' do
repository.purge_and_reseed_started_apps!
expect(AppUsageEvent.last).to match_app(process)
expect(AppUsageEvent.last.package_state).to eq('PENDING')
expect(AppUsageEvent.last.previous_package_state).to eq('UNKNOWN')
end
end
context 'and it is the current_droplet' do
it 'is STAGED' do
repository.purge_and_reseed_started_apps!
expect(AppUsageEvent.last).to match_app(process)
expect(AppUsageEvent.last.package_state).to eq('STAGED')
expect(AppUsageEvent.last.previous_package_state).to eq('UNKNOWN')
end
end
end
context 'when the latest_droplet is FAILED' do
before do
DropletModel.make(app: process.app, package: process.latest_package, state: DropletModel::FAILED_STATE)
process.reload
end
it 'is FAILED' do
repository.purge_and_reseed_started_apps!
expect(AppUsageEvent.last).to match_app(process)
expect(AppUsageEvent.last.package_state).to eq('FAILED')
expect(AppUsageEvent.last.previous_package_state).to eq('UNKNOWN')
end
end
context 'when the latest_droplet is not STAGED or FAILED' do
before do
DropletModel.make(app: process.app, package: process.latest_package, state: DropletModel::STAGING_STATE)
process.reload
end
it 'is PENDING' do
repository.purge_and_reseed_started_apps!
expect(AppUsageEvent.last).to match_app(process)
expect(AppUsageEvent.last.package_state).to eq('PENDING')
expect(AppUsageEvent.last.previous_package_state).to eq('UNKNOWN')
end
end
context 'when there is no current_droplet' do
before do
process.desired_droplet.destroy
process.reload
end
context 'and there is a package' do
it 'is PENDING' do
repository.purge_and_reseed_started_apps!
expect(AppUsageEvent.last).to match_app(process)
expect(AppUsageEvent.last.package_state).to eq('PENDING')
expect(AppUsageEvent.last.previous_package_state).to eq('UNKNOWN')
end
end
context 'and the package is FAILED' do
before do
process.latest_package.update(state: PackageModel::FAILED_STATE)
process.reload
end
it 'is FAILED' do
repository.purge_and_reseed_started_apps!
expect(AppUsageEvent.last).to match_app(process)
expect(AppUsageEvent.last.package_state).to eq('FAILED')
expect(AppUsageEvent.last.previous_package_state).to eq('UNKNOWN')
end
end
end
context 'when a new package has been added to a previously staged app' do
before do
PackageModel.make(app: process.app)
process.reload
end
it 'is PENDING' do
repository.purge_and_reseed_started_apps!
expect(AppUsageEvent.last).to match_app(process)
expect(AppUsageEvent.last.package_state).to eq('PENDING')
expect(AppUsageEvent.last.previous_package_state).to eq('UNKNOWN')
end
end
end
end
end
describe '#delete_events_older_than' do
let(:cutoff_age_in_days) { 1 }
before do
AppUsageEvent.dataset.delete
old = Time.now.utc - 999.days
3.times do
event = repository.create_from_process(ProcessModel.make)
event.created_at = old
event.save
end
end
it 'deletes events created before the specified cutoff time' do
process = ProcessModel.make
repository.create_from_process(process)
expect do
repository.delete_events_older_than(cutoff_age_in_days)
end.to change(AppUsageEvent, :count).to(1)
expect(AppUsageEvent.last).to match_app(process)
end
it 'keeps the last record even if before the cutoff age' do
expect do
repository.delete_events_older_than(cutoff_age_in_days)
end.to change(AppUsageEvent, :count).to(1)
expect(AppUsageEvent.last.created_at).to be < cutoff_age_in_days.days.ago
end
end
end
end
end