cloudfoundry/cloud_controller_ng

View on GitHub
spec/request/processes_spec.rb

Summary

Maintainability
A
0 mins
Test Coverage
require 'spec_helper'
require 'request_spec_shared_examples'

RSpec.describe 'Processes' do
  let(:org) { VCAP::CloudController::Organization.make }
  let(:space) { VCAP::CloudController::Space.make(organization: org) }
  let(:app_model) { VCAP::CloudController::AppModel.make(space: space, name: 'my_app', droplet: droplet) }
  let(:droplet) { VCAP::CloudController::DropletModel.make }
  let(:developer) { make_developer_for_space(space) }
  let(:developer_headers) { headers_for(developer, user_name:) }
  let(:user) { VCAP::CloudController::User.make }
  let(:admin_header) { admin_headers_for(user) }
  let(:user_name) { 'ProcHudson' }
  let(:build_client) { instance_double(HTTPClient, post: nil) }
  let(:metadata) do
    {
      labels: {
        release: 'stable',
        'seriouseats.com/potato' => 'mashed'
      },
      annotations: { 'checksum' => 'SHA' }
    }
  end

  before do
    allow_any_instance_of(Diego::Client).to receive(:build_client).and_return(build_client)
  end

  describe 'GET /v3/processes' do
    let!(:web_revision) { VCAP::CloudController::RevisionModel.make }

    let!(:web_process) do
      VCAP::CloudController::ProcessModel.make(
        :process,
        app: app_model,
        revision: web_revision,
        type: 'web',
        instances: 2,
        memory: 1024,
        disk_quota: 1024,
        log_rate_limit: 1_048_576,
        command: 'rackup'
      )
    end
    let!(:worker_process) do
      VCAP::CloudController::ProcessModel.make(
        :process,
        app: app_model,
        type: 'worker',
        instances: 1,
        memory: 100,
        disk_quota: 200,
        log_rate_limit: 400,
        command: 'start worker'
      )
    end

    it_behaves_like 'list query endpoint' do
      let(:message) { VCAP::CloudController::ProcessesListMessage }
      let(:request) { '/v3/processes' }
      let(:user_header) { developer_headers }

      let(:excluded_params) do
        [
          :app_guid
        ]
      end
      let(:params) do
        {
          guids: %w[foo bar],
          space_guids: %w[foo bar],
          organization_guids: %w[foo bar],
          types: %w[foo bar],
          app_guids: %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
    end

    it_behaves_like 'list_endpoint_with_common_filters' do
      let(:resource_klass) { VCAP::CloudController::ProcessModel }

      let(:api_call) do
        ->(headers, filters) { get "/v3/processes?#{filters}", nil, headers }
      end
      let(:headers) { admin_header }
    end

    it 'returns a paginated list of processes' do
      get '/v3/processes?per_page=2', nil, developer_headers

      expected_response = {
        'pagination' => {
          'total_results' => 2,
          'total_pages' => 1,
          'first' => { 'href' => "#{link_prefix}/v3/processes?page=1&per_page=2" },
          'last' => { 'href' => "#{link_prefix}/v3/processes?page=1&per_page=2" },
          'next' => nil,
          'previous' => nil
        },
        'resources' => [
          {
            'guid' => web_process.guid,
            'relationships' => {
              'app' => { 'data' => { 'guid' => app_model.guid } },
              'revision' => {
                'data' => {
                  'guid' => web_revision.guid
                }
              }
            },
            'type' => 'web',
            'command' => '[PRIVATE DATA HIDDEN IN LISTS]',
            'instances' => 2,
            'memory_in_mb' => 1024,
            'disk_in_mb' => 1024,
            'log_rate_limit_in_bytes_per_second' => 1_048_576,
            'health_check' => {
              'type' => 'port',
              'data' => {
                'timeout' => nil,
                'invocation_timeout' => nil,
                'interval' => nil
              }
            },
            'readiness_health_check' => {
              'type' => 'process',
              'data' => {
                'invocation_timeout' => nil,
                'interval' => nil
              }
            },
            'metadata' => { 'annotations' => {}, 'labels' => {} },
            'created_at' => iso8601,
            'updated_at' => iso8601,
            'version' => web_process.version,
            'links' => {
              'self' => { 'href' => "#{link_prefix}/v3/processes/#{web_process.guid}" },
              'scale' => { 'href' => "#{link_prefix}/v3/processes/#{web_process.guid}/actions/scale", 'method' => 'POST' },
              'app' => { 'href' => "#{link_prefix}/v3/apps/#{app_model.guid}" },
              'space' => { 'href' => "#{link_prefix}/v3/spaces/#{space.guid}" },
              'stats' => { 'href' => "#{link_prefix}/v3/processes/#{web_process.guid}/stats" }
            }
          },
          {
            'guid' => worker_process.guid,
            'relationships' => {
              'app' => { 'data' => { 'guid' => app_model.guid } },
              'revision' => nil
            },
            'type' => 'worker',
            'command' => '[PRIVATE DATA HIDDEN IN LISTS]',
            'instances' => 1,
            'memory_in_mb' => 100,
            'disk_in_mb' => 200,
            'log_rate_limit_in_bytes_per_second' => 400,
            'health_check' => {
              'type' => 'port',
              'data' => {
                'timeout' => nil,
                'invocation_timeout' => nil,
                'interval' => nil
              }
            },
            'readiness_health_check' => {
              'type' => 'process',
              'data' => {
                'invocation_timeout' => nil,
                'interval' => nil
              }
            },
            'metadata' => { 'annotations' => {}, 'labels' => {} },
            'created_at' => iso8601,
            'updated_at' => iso8601,
            'version' => worker_process.version,
            'links' => {
              'self' => { 'href' => "#{link_prefix}/v3/processes/#{worker_process.guid}" },
              'scale' => { 'href' => "#{link_prefix}/v3/processes/#{worker_process.guid}/actions/scale", 'method' => 'POST' },
              'app' => { 'href' => "#{link_prefix}/v3/apps/#{app_model.guid}" },
              'space' => { 'href' => "#{link_prefix}/v3/spaces/#{space.guid}" },
              'stats' => { 'href' => "#{link_prefix}/v3/processes/#{worker_process.guid}/stats" }
            }
          }
        ]
      }

      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 'filters by label selectors' do
      VCAP::CloudController::ProcessLabelModel.make(key_name: 'fruit', value: 'strawberry', process: worker_process)

      get '/v3/processes?label_selector=fruit=strawberry', {}, developer_headers

      expected_pagination = {
        'total_results' => 1,
        'total_pages' => 1,
        'first' => { 'href' => "#{link_prefix}/v3/processes?label_selector=fruit%3Dstrawberry&page=1&per_page=50" },
        'last' => { 'href' => "#{link_prefix}/v3/processes?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(worker_process.guid)
      expect(parsed_response['pagination']).to eq(expected_pagination)
    end

    context 'faceted list' do
      context 'by types' do
        it 'returns only the matching processes' do
          get '/v3/processes?per_page=2&types=worker,doesnotexist', nil, developer_headers

          expected_pagination = {
            'total_results' => 1,
            'total_pages' => 1,
            'first' => { 'href' => "#{link_prefix}/v3/processes?page=1&per_page=2&types=worker%2Cdoesnotexist" },
            'last' => { 'href' => "#{link_prefix}/v3/processes?page=1&per_page=2&types=worker%2Cdoesnotexist" },
            'next' => nil,
            'previous' => nil
          }

          expect(last_response.status).to eq(200)

          parsed_response = Oj.load(last_response.body)

          returned_guids = parsed_response['resources'].pluck('guid')
          expect(returned_guids).to contain_exactly(worker_process.guid)
          expect(parsed_response['pagination']).to be_a_response_like(expected_pagination)
        end
      end

      context 'by space_guids' do
        let(:other_space) { VCAP::CloudController::Space.make(organization: space.organization) }
        let(:other_app_model) { VCAP::CloudController::AppModel.make(space: other_space) }
        let!(:other_space_process) do
          VCAP::CloudController::ProcessModel.make(
            :process,
            app: other_app_model,
            type: 'web',
            instances: 2,
            memory: 1024,
            disk_quota: 1024,
            log_rate_limit: 1_048_576,
            command: 'rackup'
          )
        end

        before do
          other_space.add_developer developer
        end

        it 'returns only the matching processes' do
          get "/v3/processes?per_page=2&space_guids=#{other_space.guid}", nil, developer_headers

          expected_pagination = {
            'total_results' => 1,
            'total_pages' => 1,
            'first' => { 'href' => "#{link_prefix}/v3/processes?page=1&per_page=2&space_guids=#{other_space.guid}" },
            'last' => { 'href' => "#{link_prefix}/v3/processes?page=1&per_page=2&space_guids=#{other_space.guid}" },
            'next' => nil,
            'previous' => nil
          }

          expect(last_response.status).to eq(200)

          parsed_response = Oj.load(last_response.body)

          returned_guids = parsed_response['resources'].pluck('guid')
          expect(returned_guids).to contain_exactly(other_space_process.guid)
          expect(parsed_response['pagination']).to be_a_response_like(expected_pagination)
        end
      end

      context 'by organization guids' do
        let(:other_space) { VCAP::CloudController::Space.make }
        let!(:other_org) { other_space.organization }
        let!(:other_space_process) do
          VCAP::CloudController::ProcessModel.make(
            :process,
            app: other_app_model,
            type: 'web',
            instances: 2,
            memory: 1024,
            disk_quota: 1024,
            log_rate_limit: 1_048_576,
            command: 'rackup'
          )
        end
        let(:other_app_model) { VCAP::CloudController::AppModel.make(space: other_space) }
        let(:developer) { make_developer_for_space(other_space) }

        it 'returns only the matching processes' do
          get "/v3/processes?per_page=2&organization_guids=#{other_org.guid}", nil, developer_headers

          expected_pagination = {
            'total_results' => 1,
            'total_pages' => 1,
            'first' => { 'href' => "#{link_prefix}/v3/processes?organization_guids=#{other_org.guid}&page=1&per_page=2" },
            'last' => { 'href' => "#{link_prefix}/v3/processes?organization_guids=#{other_org.guid}&page=1&per_page=2" },
            'next' => nil,
            'previous' => nil
          }

          expect(last_response.status).to eq(200)

          parsed_response = Oj.load(last_response.body)

          returned_guids = parsed_response['resources'].pluck('guid')
          expect(returned_guids).to contain_exactly(other_space_process.guid)
          expect(parsed_response['pagination']).to be_a_response_like(expected_pagination)
        end
      end

      context 'by app guids' do
        let(:desired_app) { VCAP::CloudController::AppModel.make(space:) }
        let!(:desired_process) do
          VCAP::CloudController::ProcessModel.make(:process,
                                                   app: desired_app,
                                                   type: 'persnickety',
                                                   instances: 3,
                                                   memory: 2048,
                                                   disk_quota: 2048,
                                                   log_rate_limit: 2_097_152,
                                                   command: 'at ease')
        end

        it 'returns only the matching processes' do
          get "/v3/processes?per_page=2&app_guids=#{desired_app.guid}", nil, developer_headers

          expected_pagination = {
            'total_results' => 1,
            'total_pages' => 1,
            'first' => { 'href' => "#{link_prefix}/v3/processes?app_guids=#{desired_app.guid}&page=1&per_page=2" },
            'last' => { 'href' => "#{link_prefix}/v3/processes?app_guids=#{desired_app.guid}&page=1&per_page=2" },
            'next' => nil,
            'previous' => nil
          }

          expect(last_response.status).to eq(200)

          parsed_response = Oj.load(last_response.body)

          returned_guids = parsed_response['resources'].pluck('guid')
          expect(returned_guids).to contain_exactly(desired_process.guid)
          expect(parsed_response['pagination']).to be_a_response_like(expected_pagination)
        end
      end

      context 'by guids' do
        it 'returns only the matching processes' do
          get "/v3/processes?per_page=2&guids=#{web_process.guid},#{worker_process.guid}", nil, developer_headers

          expected_pagination = {
            'total_results' => 2,
            'total_pages' => 1,
            'first' => { 'href' => "#{link_prefix}/v3/processes?guids=#{web_process.guid}%2C#{worker_process.guid}&page=1&per_page=2" },
            'last' => { 'href' => "#{link_prefix}/v3/processes?guids=#{web_process.guid}%2C#{worker_process.guid}&page=1&per_page=2" },
            'next' => nil,
            'previous' => nil
          }

          expect(last_response.status).to eq(200)

          parsed_response = Oj.load(last_response.body)

          returned_guids = parsed_response['resources'].pluck('guid')
          expect(returned_guids).to contain_exactly(web_process.guid, worker_process.guid)
          expect(parsed_response['pagination']).to be_a_response_like(expected_pagination)
        end
      end
    end

    context 'permissions' do
      let(:api_call) { ->(user_headers) { get '/v3/processes', nil, user_headers } }

      let(:expected_codes_and_responses) do
        h = Hash.new(code: 200, response_guids: [web_process.guid, worker_process.guid])
        h['org_auditor'] = { code: 200, response_guids: [] }
        h['org_billing_manager'] = { code: 200, response_guids: [] }
        h['no_role'] = { code: 200, response_objects: [] }
        h
      end

      it_behaves_like 'permissions for list endpoint', ALL_PERMISSIONS
    end
  end

  describe 'GET /v3/processes/:guid' do
    let(:revision) { VCAP::CloudController::RevisionModel.make }
    let(:process) do
      VCAP::CloudController::ProcessModel.make(
        :process,
        app: app_model,
        revision: revision,
        type: 'web',
        instances: 2,
        memory: 1024,
        disk_quota: 1024,
        log_rate_limit: 1_048_576,
        command: 'rackup'
      )
    end
    let(:expected_response) do
      {
        'guid' => process.guid,
        'type' => 'web',
        'relationships' => {
          'app' => { 'data' => { 'guid' => app_model.guid } },
          'revision' => { 'data' => { 'guid' => revision.guid } }
        },
        'command' => 'rackup',
        'instances' => 2,
        'memory_in_mb' => 1024,
        'disk_in_mb' => 1024,
        'log_rate_limit_in_bytes_per_second' => 1_048_576,
        'health_check' => {
          'type' => 'port',
          'data' => {
            'timeout' => nil,
            'invocation_timeout' => nil,
            'interval' => nil
          }
        },
        'readiness_health_check' => {
          'type' => 'process',
          'data' => {
            'invocation_timeout' => nil,
            'interval' => nil
          }
        },
        'metadata' => { 'annotations' => {}, 'labels' => {} },
        'created_at' => iso8601,
        'updated_at' => iso8601,
        'version' => process.version,
        'links' => {
          'self' => { 'href' => "#{link_prefix}/v3/processes/#{process.guid}" },
          'scale' => { 'href' => "#{link_prefix}/v3/processes/#{process.guid}/actions/scale", 'method' => 'POST' },
          'app' => { 'href' => "#{link_prefix}/v3/apps/#{app_model.guid}" },
          'space' => { 'href' => "#{link_prefix}/v3/spaces/#{space.guid}" },
          'stats' => { 'href' => "#{link_prefix}/v3/processes/#{process.guid}/stats" }
        }
      }
    end

    it 'retrieves the process' do
      get "/v3/processes/#{process.guid}", nil, developer_headers

      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 'redacts information for auditors' do
      auditor = VCAP::CloudController::User.make
      space.organization.add_user(auditor)
      space.add_auditor(auditor)

      get "/v3/processes/#{process.guid}", nil, headers_for(auditor)

      parsed_response = Oj.load(last_response.body)

      expect(last_response.status).to eq(200)
      expect(parsed_response['command']).to eq('[PRIVATE DATA HIDDEN]')
    end

    context 'permissions' do
      let(:api_call) { ->(user_headers) { get "/v3/processes/#{process.guid}", nil, user_headers } }

      let(:expected_codes_and_responses) do
        h = Hash.new(code: 200, response_object: expected_response.merge({ 'command' => '[PRIVATE DATA HIDDEN]' }))
        h['space_developer'] = { code: 200, response_object: expected_response }
        h['admin'] = { code: 200, response_object: expected_response }
        h['admin_read_only'] = { code: 200, response_object: expected_response }
        h['org_auditor'] = { code: 404 }
        h['org_billing_manager'] = { code: 404, response_object: nil }
        h['no_role'] = { code: 404, response_object: nil }
        h
      end

      it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
    end
  end

  describe 'GET stats' do
    let(:process) { VCAP::CloudController::ProcessModel.make(:process, type: 'worker', app: app_model) }
    let(:net_info_1) do
      {
        address: '1.2.3.4',
        instance_address: '5.6.7.8',
        ports: [
          {
            host_port: 8080,
            container_port: 1234,
            host_tls_proxy_port: 61_002,
            container_tls_proxy_port: 61_003
          },
          {
            host_port: 3000,
            container_port: 4000,
            host_tls_proxy_port: 61_006,
            container_tls_proxy_port: 61_007
          }
        ]
      }
    end

    let(:stats_for_process) do
      {
        0 => {
          state: 'RUNNING',
          routable: true,
          details: 'some-details',
          isolation_segment: 'very-isolated',
          stats: {
            name: process.name,
            uris: process.uris,
            host: 'toast',
            net_info: net_info_1,
            uptime: 12_345,
            mem_quota: process[:memory] * 1024 * 1024,
            disk_quota: process[:disk_quota] * 1024 * 1024,
            log_rate_limit: process[:log_rate_limit],
            fds_quota: process.file_descriptors,
            usage: {
              time: usage_time,
              cpu: 0.8,
              cpu_entitlement: 0.1,
              mem: 128,
              disk: 1024,
              log_rate: 1024
            }
          }
        }
      }
    end

    let(:instances_reporters) { double(:instances_reporters) }
    let(:usage_time) { Time.now.utc.to_s }

    let(:expected_response) do
      {
        'resources' => [{
          'type' => 'worker',
          'index' => 0,
          'state' => 'RUNNING',
          'routable' => true,
          'isolation_segment' => 'very-isolated',
          'details' => 'some-details',
          'usage' => {
            'time' => usage_time,
            'cpu' => 0.8,
            'cpu_entitlement' => 0.1,
            'mem' => 128,
            'disk' => 1024,
            'log_rate' => 1024
          },
          'host' => 'toast',
          'instance_internal_ip' => '5.6.7.8',
          'instance_ports' => [
            {
              'external' => 8080,
              'internal' => 1234,
              'external_tls_proxy_port' => 61_002,
              'internal_tls_proxy_port' => 61_003
            },
            {
              'external' => 3000,
              'internal' => 4000,
              'external_tls_proxy_port' => 61_006,
              'internal_tls_proxy_port' => 61_007
            }
          ],
          'uptime' => 12_345,
          'mem_quota' => 1_073_741_824,
          'disk_quota' => 1_073_741_824,
          'fds_quota' => 16_384,
          'log_rate_limit' => 1_048_576
        }]
      }
    end

    before do
      CloudController::DependencyLocator.instance.register(:instances_reporters, instances_reporters)
      allow(instances_reporters).to receive(:stats_for_app).and_return([stats_for_process, []])
    end

    describe 'GET /v3/processes/:guid/stats' do
      context 'route integrity is enabled' do
        it 'retrieves the stats for a process' do
          get "/v3/processes/#{process.guid}/stats", nil, developer_headers

          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
      end

      context 'cpu entitlement usage is not available' do
        before do
          stats_for_process[0][:stats][:usage][:cpu_entitlement] = nil
        end

        it 'returns cpu entitlement as null' do
          get "/v3/processes/#{process.guid}/stats", nil, developer_headers

          parsed_response = Oj.load(last_response.body)

          expect(last_response.status).to eq(200)
          expect(parsed_response['resources'][0]['usage']['cpu_entitlement']).to be_nil
        end
      end
    end

    describe 'GET /v3/apps/:guid/processes/:type/stats' do
      it 'retrieves the stats for a process belonging to an app' do
        get "/v3/apps/#{app_model.guid}/processes/worker/stats", nil, developer_headers

        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
    end

    context 'permissions' do
      let(:api_call) { ->(user_headers) { get "/v3/processes/#{process.guid}/stats", nil, user_headers } }

      let(:expected_codes_and_responses) do
        h = Hash.new(code: 200, response_object: expected_response)
        h['org_auditor'] = { code: 404 }
        h['org_billing_manager'] = { code: 404, response_object: [] }
        h['no_role'] = { code: 404, response_object: [] }
        h
      end

      # while this endpoint returns a list, it's easier to test as a single object since it doesn't paginate
      it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
    end
  end

  describe 'PATCH /v3/processes/:guid' do
    let(:revision) { VCAP::CloudController::RevisionModel.make }
    let(:process) do
      VCAP::CloudController::ProcessModel.make(
        :process,
        app: app_model,
        revision: revision,
        type: 'web',
        instances: 2,
        memory: 1024,
        disk_quota: 1024,
        log_rate_limit: 1_048_576,
        command: 'rackup',
        ports: [4444, 5555],
        health_check_type: 'port',
        health_check_timeout: 10,
        health_check_interval: 5
      )
    end

    let(:update_request) do
      {
        command: 'new command',
        health_check: {
          type: 'process',
          data: {
            timeout: 20,
            interval: 5
          }
        },
        readiness_health_check: {
          type: 'port',
          data: {
            invocation_timeout: 10,
            interval: 6
          }
        },
        metadata: metadata
      }.to_json
    end

    let(:expected_response) do
      {
        'guid' => process.guid,
        'relationships' => {
          'app' => { 'data' => { 'guid' => app_model.guid } },
          'revision' => { 'data' => { 'guid' => revision.guid } }
        },
        'type' => 'web',
        'command' => 'new command',
        'instances' => 2,
        'memory_in_mb' => 1024,
        'disk_in_mb' => 1024,
        'log_rate_limit_in_bytes_per_second' => 1_048_576,
        'health_check' => {
          'type' => 'process',
          'data' => {
            'timeout' => 20,
            'invocation_timeout' => nil,
            'interval' => 5
          }
        },
        'readiness_health_check' => {
          'type' => 'port',
          'data' => {
            'invocation_timeout' => 10,
            'interval' => 6
          }
        },
        'created_at' => iso8601,
        'updated_at' => iso8601,
        'version' => process.version,
        'links' => {
          'self' => { 'href' => "#{link_prefix}/v3/processes/#{process.guid}" },
          'scale' => { 'href' => "#{link_prefix}/v3/processes/#{process.guid}/actions/scale", 'method' => 'POST' },
          'app' => { 'href' => "#{link_prefix}/v3/apps/#{app_model.guid}" },
          'space' => { 'href' => "#{link_prefix}/v3/spaces/#{space.guid}" },
          'stats' => { 'href' => "#{link_prefix}/v3/processes/#{process.guid}/stats" }
        },
        'metadata' => {
          'labels' => {
            'release' => 'stable',
            'seriouseats.com/potato' => 'mashed'
          },
          'annotations' => { 'checksum' => 'SHA' }
        }
      }
    end

    context 'permissions' do
      let(:api_call) { ->(user_headers) { patch "/v3/processes/#{process.guid}", update_request, user_headers } }

      let(:expected_codes_and_responses) do
        h = Hash.new(code: 403, errors: CF_NOT_AUTHORIZED)
        h['admin'] = { code: 200, response_object: expected_response }
        h['space_developer'] = { code: 200, response_object: expected_response }
        h['space_supporter'] = { code: 200, response_object: expected_response }
        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

      context 'when organization is suspended' do
        let(:expected_codes_and_responses) do
          h = super()
          %w[space_developer space_supporter].each { |r| h[r] = { 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

    it 'updates the process' do
      patch "/v3/processes/#{process.guid}", update_request, developer_headers.merge('CONTENT_TYPE' => 'application/json')

      parsed_response = Oj.load(last_response.body)

      expect(last_response.status).to eq(200)
      expect(parsed_response).to be_a_response_like(expected_response)

      process.reload
      expect(process.command).to eq('new command')
      expect(process.health_check_type).to eq('process')
      expect(process.health_check_timeout).to eq(20)
      expect(process.readiness_health_check_type).to eq('port')
      expect(process.readiness_health_check_invocation_timeout).to eq(10)

      event = VCAP::CloudController::Event.last
      expect(event.values).to include({
                                        type: 'audit.app.process.update',
                                        actee: app_model.guid,
                                        actee_type: 'app',
                                        actee_name: 'my_app',
                                        actor: developer.guid,
                                        actor_type: 'user',
                                        actor_username: user_name,
                                        space_guid: space.guid,
                                        organization_guid: space.organization.guid
                                      })
      expect(event.metadata).to eq({
                                     'process_guid' => process.guid,
                                     'process_type' => 'web',
                                     'request' => {
                                       'command' => '[PRIVATE DATA HIDDEN]',
                                       'health_check' => {
                                         'type' => 'process',
                                         'data' => {
                                           'timeout' => 20,
                                           'interval' => 5
                                         }
                                       },
                                       'readiness_health_check' => {
                                         'type' => 'port',
                                         'data' => {
                                           'invocation_timeout' => 10,
                                           'interval' => 6
                                         }
                                       },
                                       'metadata' => {
                                         'labels' => {
                                           'release' => 'stable',
                                           'seriouseats.com/potato' => 'mashed'
                                         },
                                         'annotations' => { 'checksum' => 'SHA' }
                                       }
                                     }
                                   })
    end
  end

  describe 'POST /v3/processes/:guid/actions/scale' do
    let(:process) do
      VCAP::CloudController::ProcessModel.make(
        :process,
        app: app_model,
        type: 'web',
        instances: 2,
        memory: 1024,
        disk_quota: 1024,
        log_rate_limit: 1_048_576,
        command: 'rackup'
      )
    end

    let(:scale_request) do
      {
        instances: 5,
        memory_in_mb: 10,
        disk_in_mb: 20,
        log_rate_limit_in_bytes_per_second: 40
      }
    end

    let(:expected_response) do
      {
        'guid' => process.guid,
        'type' => 'web',

        'relationships' => {
          'app' => { 'data' => { 'guid' => app_model.guid } },
          'revision' => nil
        },
        'command' => 'rackup',
        'instances' => 5,
        'memory_in_mb' => 10,
        'disk_in_mb' => 20,
        'log_rate_limit_in_bytes_per_second' => 40,
        'health_check' => {
          'type' => 'port',
          'data' => {
            'timeout' => nil,
            'invocation_timeout' => nil,
            'interval' => nil
          }
        },
        'readiness_health_check' => {
          'type' => 'process',
          'data' => {
            'invocation_timeout' => nil,
            'interval' => nil
          }
        },
        'metadata' => { 'annotations' => {}, 'labels' => {} },
        'created_at' => iso8601,
        'updated_at' => iso8601,
        'version' => process.version,
        'links' => {
          'self' => { 'href' => "#{link_prefix}/v3/processes/#{process.guid}" },
          'scale' => { 'href' => "#{link_prefix}/v3/processes/#{process.guid}/actions/scale", 'method' => 'POST' },
          'app' => { 'href' => "#{link_prefix}/v3/apps/#{app_model.guid}" },
          'space' => { 'href' => "#{link_prefix}/v3/spaces/#{space.guid}" },
          'stats' => { 'href' => "#{link_prefix}/v3/processes/#{process.guid}/stats" }
        }
      }
    end

    it 'scales the process' do
      post "/v3/processes/#{process.guid}/actions/scale", scale_request.to_json, developer_headers

      parsed_response = Oj.load(last_response.body)

      expect(last_response.status).to eq(202)
      expect(parsed_response).to be_a_response_like(expected_response)

      process.reload
      expect(process.instances).to eq(5)
      expect(process.memory).to eq(10)
      expect(process.disk_quota).to eq(20)
      expect(process.log_rate_limit).to eq(40)

      events = VCAP::CloudController::Event.where(actor: developer.guid).all

      process_event = events.find { |e| e.type == 'audit.app.process.scale' }
      expect(process_event.values).to include({
                                                type: 'audit.app.process.scale',
                                                actee: app_model.guid,
                                                actee_type: 'app',
                                                actee_name: 'my_app',
                                                actor: developer.guid,
                                                actor_type: 'user',
                                                actor_username: user_name,
                                                space_guid: space.guid,
                                                organization_guid: space.organization.guid
                                              })
      expect(process_event.metadata).to eq({
                                             'process_guid' => process.guid,
                                             'process_type' => 'web',
                                             'request' => {
                                               'instances' => 5,
                                               'memory_in_mb' => 10,
                                               'disk_in_mb' => 20,
                                               'log_rate_limit_in_bytes_per_second' => 40
                                             }
                                           })
    end

    context 'permissions' do
      let(:api_call) { ->(user_headers) { post "/v3/processes/#{process.guid}/actions/scale", scale_request.to_json, user_headers } }

      let(:expected_codes_and_responses) do
        h = Hash.new(code: 403, errors: CF_NOT_AUTHORIZED)
        h['admin'] = { code: 202, response_object: expected_response }
        h['space_developer'] = { code: 202, response_object: expected_response }
        h['space_supporter'] = { code: 202, response_object: expected_response }
        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

      context 'when organization is suspended' do
        let(:expected_codes_and_responses) do
          h = super()
          %w[space_developer space_supporter].each { |r| h[r] = { 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

    context 'when the user is assigned the space_supporter role' do
      let(:space_supporter) do
        user = VCAP::CloudController::User.make
        org.add_user(user)
        space.add_supporter(user)
        user
      end

      it 'can scale a process' do
        scale_request = {
          instances: 5,
          memory_in_mb: 10,
          disk_in_mb: 20,
          log_rate_limit_in_bytes_per_second: 40
        }

        post "/v3/processes/#{process.guid}/actions/scale", scale_request.to_json, headers_for(space_supporter)

        expect(last_response.status).to eq(202)
        expect(parsed_response).to be_a_response_like(expected_response)

        process.reload
        expect(process.instances).to eq(5)
        expect(process.memory).to eq(10)
        expect(process.disk_quota).to eq(20)
        expect(process.log_rate_limit).to eq(40)
      end
    end

    it 'returns a helpful error when the memory is too large' do
      scale_request = {
        memory_in_mb: 100_000_000_000
      }

      post "/v3/processes/#{process.guid}/actions/scale", scale_request.to_json, developer_headers

      expect(last_response.status).to eq(422)
      expect(parsed_response['errors'][0]['detail']).to eq 'Memory in mb must be less than or equal to 2147483647'

      process.reload
      expect(process.memory).to eq(1024)
    end

    it 'ensures that the memory allocation is greater than existing sidecar memory allocation' do
      sidecar = VCAP::CloudController::SidecarModel.make(
        name: 'my-sidecar',
        app: app_model,
        memory: 256
      )
      VCAP::CloudController::SidecarProcessTypeModel.make(sidecar: sidecar, type: process.type, app_guid: app_model.guid)

      scale_request = {
        memory_in_mb: 256
      }

      post "/v3/processes/#{process.guid}/actions/scale", scale_request.to_json, developer_headers

      expect(last_response.status).to eq(422)
      expect(parsed_response['errors'][0]['detail']).to eq 'The requested memory allocation is not large enough to run all of your sidecar processes'

      process.reload
      expect(process.memory).to eq(1024)
    end

    it 'returns a helpful error when the log quota is too small' do
      scale_request = {
        log_rate_limit_in_bytes_per_second: -2
      }

      post "/v3/processes/#{process.guid}/actions/scale", scale_request.to_json, developer_headers

      expect(last_response.status).to eq(422)
      expect(parsed_response['errors'][0]['detail']).to eq 'Log rate limit in bytes per second must be greater than or equal to -1'

      process.reload
      expect(process.log_rate_limit).to eq(1_048_576)
    end

    context 'telemetry' do
      let(:process) do
        VCAP::CloudController::ProcessModel.make(
          :process,
          app: app_model,
          type: 'web',
          instances: 2,
          memory: 1024,
          disk_quota: 1024,
          log_rate_limit: 1_048_576,
          command: 'rackup'
        )
      end

      let(:scale_request) do
        {
          instances: 5,
          memory_in_mb: 10,
          disk_in_mb: 20,
          log_rate_limit_in_bytes_per_second: 40
        }
      end

      it 'logs the required fields when the process gets scaled' do
        Timecop.freeze do
          expected_json = {
            'telemetry-source' => 'cloud_controller_ng',
            'telemetry-time' => Time.now.to_datetime.rfc3339,
            'scale-app' => {
              'api-version' => 'v3',
              'instance-count' => 5,
              'memory-in-mb' => 10,
              'disk-in-mb' => 20,
              'log-rate-in-bytes-per-second' => 40,
              'process-type' => 'web',
              'app-id' => OpenSSL::Digest::SHA256.hexdigest(process.app.guid),
              'user-id' => OpenSSL::Digest::SHA256.hexdigest(developer.guid)
            }
          }
          expect_any_instance_of(ActiveSupport::Logger).to receive(:info).with(Oj.dump(expected_json))
          post "/v3/processes/#{process.guid}/actions/scale", scale_request.to_json, developer_headers

          expect(last_response.status).to eq(202), last_response.body
        end
      end
    end
  end

  describe 'DELETE /v3/processes/:guid/instances/:index' do
    before do
      allow_any_instance_of(VCAP::CloudController::Diego::BbsAppsClient).to receive(:stop_index)
    end

    it 'terminates a single instance of a process' do
      process = VCAP::CloudController::ProcessModel.make(:process, type: 'web', app: app_model)

      delete "/v3/processes/#{process.guid}/instances/0", nil, developer_headers

      expect(last_response.status).to eq(204)

      events        = VCAP::CloudController::Event.where(actor: developer.guid).all
      process_event = events.find { |e| e.type == 'audit.app.process.terminate_instance' }
      expect(process_event.values).to include({
                                                type: 'audit.app.process.terminate_instance',
                                                actee: app_model.guid,
                                                actee_type: 'app',
                                                actee_name: 'my_app',
                                                actor: developer.guid,
                                                actor_type: 'user',
                                                actor_username: user_name,
                                                space_guid: space.guid,
                                                organization_guid: space.organization.guid
                                              })
      expect(process_event.metadata).to eq({
                                             'process_guid' => process.guid,
                                             'process_type' => 'web',
                                             'process_index' => 0
                                           })
    end

    context 'permissions' do
      let(:process) { VCAP::CloudController::ProcessModel.make(:process, type: 'web', app: app_model) }

      let(:api_call) { ->(user_headers) { delete "/v3/processes/#{process.guid}/instances/0", nil, user_headers } }

      let(:expected_codes_and_responses) do
        h = Hash.new(code: 403, errors: CF_NOT_AUTHORIZED)
        h['admin'] = { code: 204 }
        h['space_developer'] = { code: 204 }
        h['space_supporter'] = { code: 204 }
        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

      context 'when organization is suspended' do
        let(:expected_codes_and_responses) do
          h = super()
          %w[space_developer space_supporter].each { |r| h[r] = { 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/apps/:guid/processes' do
    let!(:process1) do
      VCAP::CloudController::ProcessModel.make(
        :process,
        app: app_model,
        type: 'web',
        instances: 2,
        memory: 1024,
        disk_quota: 1024,
        log_rate_limit: 1_048_576,
        command: 'rackup'
      )
    end

    let!(:process2) do
      VCAP::CloudController::ProcessModel.make(
        :process,
        app: app_model,
        revision: revision2,
        type: 'worker',
        instances: 1,
        memory: 100,
        disk_quota: 200,
        log_rate_limit: 400,
        command: 'start worker'
      )
    end

    let!(:process3) do
      VCAP::CloudController::ProcessModel.make(:process, app: app_model, revision: revision3)
    end

    let!(:deployment_process) do
      VCAP::CloudController::ProcessModel.make(:process, app: app_model, type: 'web-deployment', revision: deployment_revision)
    end
    let!(:revision3) { VCAP::CloudController::RevisionModel.make }
    let!(:revision2) { VCAP::CloudController::RevisionModel.make }
    let!(:deployment_revision) { VCAP::CloudController::RevisionModel.make }

    it_behaves_like 'list_endpoint_with_common_filters' do
      let(:resource_klass) { VCAP::CloudController::ProcessModel }
      let(:additional_resource_params) { { app: app_model } }
      let(:api_call) do
        ->(headers, filters) { get "/v3/apps/#{app_model.guid}/processes?#{filters}", nil, headers }
      end
      let(:headers) { admin_headers }
    end

    it 'returns a paginated list of processes for an app' do
      get "/v3/apps/#{app_model.guid}/processes?per_page=2", nil, developer_headers

      expected_response = {
        'pagination' => {
          'total_results' => 4,
          'total_pages' => 2,
          'first' => { 'href' => "#{link_prefix}/v3/apps/#{app_model.guid}/processes?page=1&per_page=2" },
          'last' => { 'href' => "#{link_prefix}/v3/apps/#{app_model.guid}/processes?page=2&per_page=2" },
          'next' => { 'href' => "#{link_prefix}/v3/apps/#{app_model.guid}/processes?page=2&per_page=2" },
          'previous' => nil
        },
        'resources' => [
          {
            'guid' => process1.guid,
            'relationships' => {
              'app' => { 'data' => { 'guid' => app_model.guid } },
              'revision' => nil
            },
            'type' => 'web',
            'command' => '[PRIVATE DATA HIDDEN IN LISTS]',
            'instances' => 2,
            'memory_in_mb' => 1024,
            'disk_in_mb' => 1024,
            'log_rate_limit_in_bytes_per_second' => 1_048_576,
            'health_check' => {
              'type' => 'port',
              'data' => {
                'timeout' => nil,
                'invocation_timeout' => nil,
                'interval' => nil
              }
            },
            'readiness_health_check' => {
              'type' => 'process',
              'data' => {
                'invocation_timeout' => nil,
                'interval' => nil
              }
            },
            'metadata' => { 'annotations' => {}, 'labels' => {} },
            'created_at' => iso8601,
            'updated_at' => iso8601,
            'version' => process1.version,
            'links' => {
              'self' => { 'href' => "#{link_prefix}/v3/processes/#{process1.guid}" },
              'scale' => { 'href' => "#{link_prefix}/v3/processes/#{process1.guid}/actions/scale", 'method' => 'POST' },
              'app' => { 'href' => "#{link_prefix}/v3/apps/#{app_model.guid}" },
              'space' => { 'href' => "#{link_prefix}/v3/spaces/#{space.guid}" },
              'stats' => { 'href' => "#{link_prefix}/v3/processes/#{process1.guid}/stats" }
            }
          },
          {
            'guid' => process2.guid,
            'relationships' => {
              'app' => { 'data' => { 'guid' => app_model.guid } },
              'revision' => {
                'data' => {
                  'guid' => revision2.guid
                }
              }
            },
            'type' => 'worker',
            'command' => '[PRIVATE DATA HIDDEN IN LISTS]',
            'instances' => 1,
            'memory_in_mb' => 100,
            'disk_in_mb' => 200,
            'log_rate_limit_in_bytes_per_second' => 400,
            'health_check' => {
              'type' => 'port',
              'data' => {
                'timeout' => nil,
                'invocation_timeout' => nil,
                'interval' => nil
              }
            },
            'readiness_health_check' => {
              'type' => 'process',
              'data' => {
                'invocation_timeout' => nil,
                'interval' => nil
              }
            },
            'metadata' => { 'annotations' => {}, 'labels' => {} },
            'created_at' => iso8601,
            'updated_at' => iso8601,
            'version' => process2.version,
            'links' => {
              'self' => { 'href' => "#{link_prefix}/v3/processes/#{process2.guid}" },
              'scale' => { 'href' => "#{link_prefix}/v3/processes/#{process2.guid}/actions/scale", 'method' => 'POST' },
              'app' => { 'href' => "#{link_prefix}/v3/apps/#{app_model.guid}" },
              'space' => { 'href' => "#{link_prefix}/v3/spaces/#{space.guid}" },
              'stats' => { 'href' => "#{link_prefix}/v3/processes/#{process2.guid}/stats" }
            }
          }
        ]
      }

      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

    context 'faceted list' do
      context 'by types' do
        it 'returns only the matching processes' do
          get "/v3/apps/#{app_model.guid}/processes?per_page=2&types=worker", nil, developer_headers

          expected_pagination = {
            'total_results' => 1,
            'total_pages' => 1,
            'first' => { 'href' => "#{link_prefix}/v3/apps/#{app_model.guid}/processes?page=1&per_page=2&types=worker" },
            'last' => { 'href' => "#{link_prefix}/v3/apps/#{app_model.guid}/processes?page=1&per_page=2&types=worker" },
            'next' => nil,
            'previous' => nil
          }

          expect(last_response.status).to eq(200)

          parsed_response = Oj.load(last_response.body)

          returned_guids = parsed_response['resources'].pluck('guid')
          expect(returned_guids).to contain_exactly(process2.guid)
          expect(parsed_response['pagination']).to be_a_response_like(expected_pagination)
        end
      end

      context 'by guids' do
        it 'returns only the matching processes' do
          get "/v3/apps/#{app_model.guid}/processes?per_page=2&guids=#{process1.guid},#{process2.guid}", nil, developer_headers

          expected_pagination = {
            'total_results' => 2,
            'total_pages' => 1,
            'first' => { 'href' => "#{link_prefix}/v3/apps/#{app_model.guid}/processes?guids=#{process1.guid}%2C#{process2.guid}&page=1&per_page=2" },
            'last' => { 'href' => "#{link_prefix}/v3/apps/#{app_model.guid}/processes?guids=#{process1.guid}%2C#{process2.guid}&page=1&per_page=2" },
            'next' => nil,
            'previous' => nil
          }

          expect(last_response.status).to eq(200)

          parsed_response = Oj.load(last_response.body)

          returned_guids = parsed_response['resources'].pluck('guid')
          expect(returned_guids).to contain_exactly(process1.guid, process2.guid)
          expect(parsed_response['pagination']).to be_a_response_like(expected_pagination)
        end
      end
    end

    context 'permissions' do
      let(:api_call) { ->(user_headers) { get "/v3/apps/#{app_model.guid}/processes", nil, user_headers } }
      let(:expected_guids) { [process1.guid, process2.guid, process3.guid, deployment_process.guid] }

      let(:expected_codes_and_responses) do
        h = Hash.new(code: 200, response_guids: expected_guids)
        h['org_auditor'] = { code: 404 }
        h['org_billing_manager'] = { code: 404, response_guids: nil }
        h['no_role'] = { code: 404, response_guids: nil }
        h
      end

      it_behaves_like 'permissions for list endpoint', ALL_PERMISSIONS
    end
  end

  describe 'GET /v3/apps/:guid/processes/:type' do
    let!(:process) do
      VCAP::CloudController::ProcessModel.make(
        :process,
        app: app_model,
        revision: revision,
        type: 'web',
        instances: 2,
        memory: 1024,
        disk_quota: 1024,
        log_rate_limit: 1_048_576,
        command: 'rackup'
      )
    end
    let!(:revision) { VCAP::CloudController::RevisionModel.make }
    let(:expected_response) do
      {
        'guid' => process.guid,
        'relationships' => {
          'app' => { 'data' => { 'guid' => app_model.guid } },
          'revision' => { 'data' => { 'guid' => revision.guid } }
        },
        'type' => 'web',
        'command' => 'rackup',
        'instances' => 2,
        'memory_in_mb' => 1024,
        'disk_in_mb' => 1024,
        'log_rate_limit_in_bytes_per_second' => 1_048_576,
        'health_check' => {
          'type' => 'port',
          'data' => {
            'timeout' => nil,
            'invocation_timeout' => nil,
            'interval' => nil
          }
        },
        'readiness_health_check' => {
          'type' => 'process',
          'data' => {
            'invocation_timeout' => nil,
            'interval' => nil
          }
        },
        'metadata' => { 'annotations' => {}, 'labels' => {} },
        'created_at' => iso8601,
        'updated_at' => iso8601,
        'version' => process.version,
        'links' => {
          'self' => { 'href' => "#{link_prefix}/v3/processes/#{process.guid}" },
          'scale' => { 'href' => "#{link_prefix}/v3/processes/#{process.guid}/actions/scale", 'method' => 'POST' },
          'app' => { 'href' => "#{link_prefix}/v3/apps/#{app_model.guid}" },
          'space' => { 'href' => "#{link_prefix}/v3/spaces/#{space.guid}" },
          'stats' => { 'href' => "#{link_prefix}/v3/processes/#{process.guid}/stats" }
        }
      }
    end

    it 'retrieves the process for an app with the requested type' do
      get "/v3/apps/#{app_model.guid}/processes/web", nil, developer_headers

      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 'redacts information for auditors' do
      VCAP::CloudController::ProcessModel.make(:process, app: app_model, type: 'web', command: 'rackup')

      auditor = VCAP::CloudController::User.make
      space.organization.add_user(auditor)
      space.add_auditor(auditor)

      get "/v3/apps/#{app_model.guid}/processes/web", nil, headers_for(auditor)

      parsed_response = Oj.load(last_response.body)

      expect(last_response.status).to eq(200)
      expect(parsed_response['command']).to eq('[PRIVATE DATA HIDDEN]')
    end

    context 'permissions' do
      let(:api_call) { ->(user_headers) { get "/v3/apps/#{app_model.guid}/processes/web", nil, user_headers } }

      let(:expected_codes_and_responses) do
        h = Hash.new(code: 200, response_object: expected_response.merge({ 'command' => '[PRIVATE DATA HIDDEN]' }))
        h['space_developer'] = { code: 200, response_object: expected_response }
        h['admin'] = { code: 200, response_object: expected_response }
        h['admin_read_only'] = { code: 200, response_object: expected_response }
        h['org_billing_manager'] = { code: 404, response_object: nil }
        h['org_auditor'] = { code: 404, response_object: nil }
        h['no_role'] = { code: 404, response_object: nil }
        h
      end

      it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
    end
  end

  describe 'PATCH /v3/apps/:guid/processes/:type' do
    it 'updates the process' do
      process = VCAP::CloudController::ProcessModel.make(
        :process,
        app: app_model,
        type: 'web',
        instances: 2,
        memory: 1024,
        disk_quota: 1024,
        log_rate_limit: 1_048_576,
        command: 'rackup',
        ports: [4444, 5555],
        health_check_type: 'port',
        health_check_timeout: 10,
        readiness_health_check_type: 'process'
      )

      update_request = {
        command: 'new command',
        health_check: {
          type: 'http',
          data: {
            timeout: 20,
            endpoint: '/healthcheck'
          }
        },
        readiness_health_check: {
          type: 'http',
          data: {
            invocation_timeout: 10,
            interval: 7,
            endpoint: '/ready'
          }
        },
        metadata: metadata
      }.to_json

      patch "/v3/apps/#{app_model.guid}/processes/web", update_request, developer_headers.merge('CONTENT_TYPE' => 'application/json')

      expected_response = {
        'guid' => process.guid,
        'relationships' => {
          'app' => { 'data' => { 'guid' => app_model.guid } },
          'revision' => nil
        },
        'type' => 'web',
        'command' => 'new command',
        'instances' => 2,
        'memory_in_mb' => 1024,
        'disk_in_mb' => 1024,
        'log_rate_limit_in_bytes_per_second' => 1_048_576,
        'health_check' => {
          'type' => 'http',
          'data' => {
            'timeout' => 20,
            'endpoint' => '/healthcheck',
            'invocation_timeout' => nil,
            'interval' => nil
          }
        },
        'readiness_health_check' => {
          'type' => 'http',
          'data' => {
            'endpoint' => '/ready',
            'invocation_timeout' => 10,
            'interval' => 7
          }
        },
        'created_at' => iso8601,
        'updated_at' => iso8601,
        'version' => process.version,
        'links' => {
          'self' => { 'href' => "#{link_prefix}/v3/processes/#{process.guid}" },
          'scale' => { 'href' => "#{link_prefix}/v3/processes/#{process.guid}/actions/scale", 'method' => 'POST' },
          'app' => { 'href' => "#{link_prefix}/v3/apps/#{app_model.guid}" },
          'space' => { 'href' => "#{link_prefix}/v3/spaces/#{space.guid}" },
          'stats' => { 'href' => "#{link_prefix}/v3/processes/#{process.guid}/stats" }
        },
        '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).to be_a_response_like(expected_response)

      process.reload
      expect(process.command).to eq('new command')
      expect(process.health_check_type).to eq('http')
      expect(process.health_check_timeout).to eq(20)
      expect(process.health_check_http_endpoint).to eq('/healthcheck')

      event = VCAP::CloudController::Event.last
      expect(event.values).to include({
                                        type: 'audit.app.process.update',
                                        actee: app_model.guid,
                                        actee_type: 'app',
                                        actee_name: 'my_app',
                                        actor: developer.guid,
                                        actor_type: 'user',
                                        actor_username: user_name,
                                        space_guid: space.guid,
                                        organization_guid: space.organization.guid
                                      })
      expect(event.metadata).to eq({
                                     'process_guid' => process.guid,
                                     'process_type' => 'web',
                                     'request' => {
                                       'command' => '[PRIVATE DATA HIDDEN]',
                                       'health_check' => {
                                         'type' => 'http',
                                         'data' => {
                                           'timeout' => 20,
                                           'endpoint' => '/healthcheck'
                                         }
                                       },
                                       'readiness_health_check' => {
                                         'type' => 'http',
                                         'data' => {
                                           'endpoint' => '/ready',
                                           'invocation_timeout' => 10,
                                           'interval' => 7
                                         }
                                       },
                                       'metadata' => {
                                         'labels' => {
                                           'release' => 'stable',
                                           'seriouseats.com/potato' => 'mashed'
                                         },
                                         'annotations' => { 'checksum' => 'SHA' }
                                       }
                                     }
                                   })
    end
  end

  describe 'POST /v3/apps/:guid/processes/:type/actions/scale' do
    let!(:process) do
      VCAP::CloudController::ProcessModel.make(
        :process,
        app: app_model,
        type: 'web',
        instances: 2,
        memory: 1024,
        disk_quota: 1024,
        log_rate_limit: 1_048_576,
        command: 'rackup',
        state: 'STARTED'
      )
    end

    let(:scale_request) do
      {
        instances: 5,
        memory_in_mb: 10,
        disk_in_mb: 20,
        log_rate_limit_in_bytes_per_second: 40
      }
    end

    let(:expected_response) do
      {
        'guid' => process.guid,
        'type' => 'web',

        'relationships' => {
          'app' => { 'data' => { 'guid' => app_model.guid } },
          'revision' => nil
        },
        'command' => 'rackup',
        'instances' => 5,
        'memory_in_mb' => 10,
        'disk_in_mb' => 20,
        'log_rate_limit_in_bytes_per_second' => 40,
        'health_check' => {
          'type' => 'port',
          'data' => {
            'timeout' => nil,
            'invocation_timeout' => nil,
            'interval' => nil
          }
        },
        'readiness_health_check' => {
          'type' => 'process',
          'data' => {
            'invocation_timeout' => nil,
            'interval' => nil
          }
        },
        'metadata' => { 'annotations' => {}, 'labels' => {} },
        'created_at' => iso8601,
        'updated_at' => iso8601,
        'version' => process.version,
        'links' => {
          'self' => { 'href' => "#{link_prefix}/v3/processes/#{process.guid}" },
          'scale' => { 'href' => "#{link_prefix}/v3/processes/#{process.guid}/actions/scale", 'method' => 'POST' },
          'app' => { 'href' => "#{link_prefix}/v3/apps/#{app_model.guid}" },
          'space' => { 'href' => "#{link_prefix}/v3/spaces/#{space.guid}" },
          'stats' => { 'href' => "#{link_prefix}/v3/processes/#{process.guid}/stats" }
        }
      }
    end

    it 'scales the process belonging to an app' do
      post "/v3/apps/#{app_model.guid}/processes/web/actions/scale", scale_request.to_json, developer_headers
      parsed_response = Oj.load(last_response.body)

      expect(last_response.status).to eq(202)
      expect(parsed_response).to be_a_response_like(expected_response)

      process.reload
      expect(process.instances).to eq(5)
      expect(process.memory).to eq(10)
      expect(process.disk_quota).to eq(20)
      expect(process.log_rate_limit).to eq(40)

      events = VCAP::CloudController::Event.where(actor: developer.guid).all

      process_event = events.find { |e| e.type == 'audit.app.process.scale' }
      expect(process_event.values).to include({
                                                type: 'audit.app.process.scale',
                                                actee: app_model.guid,
                                                actee_type: 'app',
                                                actee_name: 'my_app',
                                                actor: developer.guid,
                                                actor_type: 'user',
                                                actor_username: user_name,
                                                space_guid: space.guid,
                                                organization_guid: space.organization.guid
                                              })
      expect(process_event.metadata).to eq({
                                             'process_guid' => process.guid,
                                             'process_type' => 'web',
                                             'request' => {
                                               'instances' => 5,
                                               'memory_in_mb' => 10,
                                               'disk_in_mb' => 20,
                                               'log_rate_limit_in_bytes_per_second' => 40
                                             }
                                           })
    end

    context 'when the log rate limit would be exceeded by adding additional instances' do
      let(:scale_request) do
        {
          instances: 5
        }
      end

      before do
        org.update(quota_definition: VCAP::CloudController::QuotaDefinition.make(log_rate_limit: 2_097_152))
      end

      it 'fails to scale the process' do
        post "/v3/apps/#{app_model.guid}/processes/web/actions/scale", scale_request.to_json, developer_headers

        expect(last_response.status).to eq(422)
        expect(parsed_response['errors'][0]['detail']).to eq 'log_rate_limit exceeds organization log rate quota'

        process.reload
        expect(process.instances).to eq(2)
      end
    end

    context 'when the user is assigned the space_supporter role' do
      let(:space_supporter) do
        user = VCAP::CloudController::User.make
        org.add_user(user)
        space.add_supporter(user)
        user
      end

      it 'can scale a process' do
        post "/v3/apps/#{app_model.guid}/processes/web/actions/scale", scale_request.to_json, headers_for(space_supporter)

        expect(last_response.status).to eq(202)
        expect(parsed_response).to be_a_response_like(expected_response)

        process.reload
        expect(process.instances).to eq(5)
        expect(process.memory).to eq(10)
        expect(process.disk_quota).to eq(20)
        expect(process.log_rate_limit).to eq(40)
      end
    end

    context 'telemetry' do
      it 'logs the required fields when the process gets scaled' do
        Timecop.freeze do
          expected_json = {
            'telemetry-source' => 'cloud_controller_ng',
            'telemetry-time' => Time.now.to_datetime.rfc3339,
            'scale-app' => {
              'api-version' => 'v3',
              'instance-count' => 5,
              'memory-in-mb' => 10,
              'disk-in-mb' => 20,
              'log-rate-in-bytes-per-second' => 40,
              'process-type' => 'web',
              'app-id' => OpenSSL::Digest::SHA256.hexdigest(app_model.guid),
              'user-id' => OpenSSL::Digest::SHA256.hexdigest(developer.guid)
            }
          }
          expect_any_instance_of(ActiveSupport::Logger).to receive(:info).with(Oj.dump(expected_json))

          post "/v3/apps/#{app_model.guid}/processes/web/actions/scale", scale_request.to_json, developer_headers

          expect(last_response.status).to eq(202), last_response.body
        end
      end
    end
  end

  describe 'DELETE /v3/apps/:guid/processes/:type/instances/:index' do
    before do
      allow_any_instance_of(VCAP::CloudController::Diego::BbsAppsClient).to receive(:stop_index)
    end

    let!(:process) { VCAP::CloudController::ProcessModel.make(:process, type: 'web', app: app_model) }

    it 'terminates a single instance of a process belonging to an app' do
      delete "/v3/apps/#{app_model.guid}/processes/web/instances/0", nil, developer_headers

      expect(last_response.status).to eq(204)

      events        = VCAP::CloudController::Event.where(actor: developer.guid).all
      process_event = events.find { |e| e.type == 'audit.app.process.terminate_instance' }
      expect(process_event.values).to include({
                                                type: 'audit.app.process.terminate_instance',
                                                actee: app_model.guid,
                                                actee_type: 'app',
                                                actee_name: 'my_app',
                                                actor: developer.guid,
                                                actor_type: 'user',
                                                actor_username: user_name,
                                                space_guid: space.guid,
                                                organization_guid: space.organization.guid
                                              })
      expect(process_event.metadata).to eq({
                                             'process_guid' => process.guid,
                                             'process_type' => 'web',
                                             'process_index' => 0
                                           })
    end

    context 'permissions' do
      let(:api_call) { ->(user_headers) { delete "/v3/apps/#{app_model.guid}/processes/web/instances/0", nil, user_headers } }

      let(:expected_codes_and_responses) do
        h = Hash.new(code: 403)
        h['admin'] = { code: 204 }
        h['space_developer'] = { code: 204 }
        h['space_supporter'] = { code: 204 }
        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
  end
end