cloudfoundry/cloud_controller_ng

View on GitHub
spec/request/buildpacks_spec.rb

Summary

Maintainability
C
7 hrs
Test Coverage
require 'spec_helper'
require 'messages/buildpack_upload_message'
require 'request_spec_shared_examples'

RSpec.describe 'buildpacks' do
  describe 'GET /v3/buildpacks' do
    let(:user) { make_user }
    let(:headers) { headers_for(user) }

    before do
      TestConfig.override(kubernetes: {})
    end

    it 'returns 200 OK' do
      get '/v3/buildpacks', nil, headers
      expect(last_response.status).to eq(200)
    end

    it_behaves_like 'list query endpoint' do
      let(:request) { 'v3/buildpacks' }

      let(:message) { VCAP::CloudController::BuildpacksListMessage }
      let(:user_header) { headers }
      let(:params) do
        {
          page: '2',
          per_page: '10',
          order_by: 'updated_at',
          names: 'foo',
          stacks: 'cf',
          label_selector: 'foo,bar',
          guids: '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::Buildpack }
      let(:api_call) do
        ->(headers, filters) { get "/v3/buildpacks?#{filters}", nil, headers }
      end
      let(:headers) { admin_headers }
    end

    context 'when filtered by label_selector' do
      let!(:buildpackA) { VCAP::CloudController::Buildpack.make(name: 'A') }
      let!(:buildpackAFruit) { VCAP::CloudController::BuildpackLabelModel.make(key_name: 'fruit', value: 'strawberry', buildpack: buildpackA) }
      let!(:buildpackAAnimal) { VCAP::CloudController::BuildpackLabelModel.make(key_name: 'animal', value: 'horse', buildpack: buildpackA) }

      let!(:buildpackB) { VCAP::CloudController::Buildpack.make(name: 'B') }
      let!(:buildpackBEnv) { VCAP::CloudController::BuildpackLabelModel.make(key_name: 'env', value: 'prod', buildpack: buildpackB) }
      let!(:buildpackBAnimal) { VCAP::CloudController::BuildpackLabelModel.make(key_name: 'animal', value: 'dog', buildpack: buildpackB) }

      let!(:buildpackC) { VCAP::CloudController::Buildpack.make(name: 'C') }
      let!(:buildpackCEnv) { VCAP::CloudController::BuildpackLabelModel.make(key_name: 'env', value: 'prod', buildpack: buildpackC) }
      let!(:buildpackCAnimal) { VCAP::CloudController::BuildpackLabelModel.make(key_name: 'animal', value: 'horse', buildpack: buildpackC) }

      let!(:buildpackD) { VCAP::CloudController::Buildpack.make(name: 'D') }
      let!(:buildpackDEnv) { VCAP::CloudController::BuildpackLabelModel.make(key_name: 'env', value: 'prod', buildpack: buildpackD) }

      let!(:buildpackE) { VCAP::CloudController::Buildpack.make(name: 'E') }
      let!(:buildpackEEnv) { VCAP::CloudController::BuildpackLabelModel.make(key_name: 'env', value: 'staging', buildpack: buildpackE) }
      let!(:buildpackEAnimal) { VCAP::CloudController::BuildpackLabelModel.make(key_name: 'animal', value: 'dog', buildpack: buildpackE) }

      it 'returns the matching buildpacks' do
        get '/v3/buildpacks?label_selector=!fruit,env=prod,animal in (dog,horse)', nil, admin_headers
        expect(last_response.status).to eq(200), last_response.body

        parsed_response = MultiJson.load(last_response.body)
        expect(parsed_response['resources'].pluck('guid')).to contain_exactly(buildpackB.guid, buildpackC.guid)
      end
    end

    context 'when filtered by null stack' do
      let!(:stack) { VCAP::CloudController::Stack.make }
      let!(:buildpack_without_stack) { VCAP::CloudController::Buildpack.make(stack: nil) }
      let!(:buildpack_with_stack) { VCAP::CloudController::Buildpack.make(stack: stack.name) }

      it 'returns the matching buildpacks' do
        get '/v3/buildpacks?stacks=', nil, admin_headers
        expect(last_response.status).to eq(200), last_response.body

        parsed_response = MultiJson.load(last_response.body)
        expect(parsed_response['resources'].pluck('guid')).to contain_exactly(buildpack_without_stack.guid)
      end
    end

    context 'When buildpacks exist' do
      let!(:stack1) { VCAP::CloudController::Stack.make }
      let!(:stack2) { VCAP::CloudController::Stack.make }
      let!(:stack3) { VCAP::CloudController::Stack.make }

      let!(:buildpack1) { VCAP::CloudController::Buildpack.make(stack: stack1.name, position: 1) }
      let!(:buildpack2) { VCAP::CloudController::Buildpack.make(stack: stack2.name, position: 3) }
      let!(:buildpack3) { VCAP::CloudController::Buildpack.make(stack: stack3.name, position: 2) }

      it 'returns a paginated list of buildpacks, sorted by position' do
        get '/v3/buildpacks?page=1&per_page=2', nil, headers

        expect(parsed_response).to be_a_response_like(
          {
            'pagination' => {
              'total_results' => 3,
              'total_pages' => 2,
              'first' => {
                'href' => "#{link_prefix}/v3/buildpacks?page=1&per_page=2"
              },
              'last' => {
                'href' => "#{link_prefix}/v3/buildpacks?page=2&per_page=2"
              },
              'next' => {
                'href' => "#{link_prefix}/v3/buildpacks?page=2&per_page=2"
              },
              'previous' => nil
            },
            'resources' => [
              {
                'guid' => buildpack1.guid,
                'created_at' => iso8601,
                'updated_at' => iso8601,
                'name' => buildpack1.name,
                'state' => buildpack1.state,
                'filename' => buildpack1.filename,
                'stack' => buildpack1.stack,
                'position' => 1,
                'enabled' => true,
                'locked' => false,
                'metadata' => { 'labels' => {}, 'annotations' => {} },
                'links' => {
                  'self' => {
                    'href' => "#{link_prefix}/v3/buildpacks/#{buildpack1.guid}"
                  },
                  'upload' => {
                    'href' => "#{link_prefix}/v3/buildpacks/#{buildpack1.guid}/upload",
                    'method' => 'POST'
                  }
                }
              },
              {
                'guid' => buildpack3.guid,
                'created_at' => iso8601,
                'updated_at' => iso8601,
                'name' => buildpack3.name,
                'state' => buildpack3.state,
                'filename' => buildpack3.filename,
                'stack' => buildpack3.stack,
                'position' => 2,
                'enabled' => true,
                'locked' => false,
                'metadata' => { 'labels' => {}, 'annotations' => {} },
                'links' => {
                  'self' => {
                    'href' => "#{link_prefix}/v3/buildpacks/#{buildpack3.guid}"
                  },
                  'upload' => {
                    'href' => "#{link_prefix}/v3/buildpacks/#{buildpack3.guid}/upload",
                    'method' => 'POST'
                  }
                }
              }
            ]
          }
        )
      end

      it 'returns a list of filtered buildpacks' do
        get "/v3/buildpacks?names=#{buildpack1.name},#{buildpack3.name}&stacks=#{stack1.name}", nil, headers

        expect(parsed_response).to be_a_response_like(
          {
            'pagination' => {
              'total_results' => 1,
              'total_pages' => 1,
              'first' => {
                'href' => "#{link_prefix}/v3/buildpacks?names=#{buildpack1.name}%2C#{buildpack3.name}&page=1&per_page=50&stacks=#{stack1.name}"
              },
              'last' => {
                'href' => "#{link_prefix}/v3/buildpacks?names=#{buildpack1.name}%2C#{buildpack3.name}&page=1&per_page=50&stacks=#{stack1.name}"
              },
              'next' => nil,
              'previous' => nil
            },
            'resources' => [
              {
                'guid' => buildpack1.guid,
                'created_at' => iso8601,
                'updated_at' => iso8601,
                'name' => buildpack1.name,
                'state' => buildpack1.state,
                'filename' => buildpack1.filename,
                'stack' => stack1.name,
                'position' => 1,
                'enabled' => true,
                'locked' => false,
                'metadata' => { 'labels' => {}, 'annotations' => {} },
                'links' => {
                  'self' => {
                    'href' => "#{link_prefix}/v3/buildpacks/#{buildpack1.guid}"
                  },
                  'upload' => {
                    'href' => "#{link_prefix}/v3/buildpacks/#{buildpack1.guid}/upload",
                    'method' => 'POST'
                  }
                }
              }
            ]
          }
        )
      end

      it 'orders by position' do
        get "/v3/buildpacks?names=#{buildpack1.name},#{buildpack3.name}&order_by=-position", nil, headers

        expect(parsed_response).to be_a_response_like(
          {
            'pagination' => {
              'total_results' => 2,
              'total_pages' => 1,
              'first' => {
                'href' => "#{link_prefix}/v3/buildpacks?names=#{buildpack1.name}%2C#{buildpack3.name}&order_by=-position&page=1&per_page=50"
              },
              'last' => {
                'href' => "#{link_prefix}/v3/buildpacks?names=#{buildpack1.name}%2C#{buildpack3.name}&order_by=-position&page=1&per_page=50"
              },
              'next' => nil,
              'previous' => nil
            },
            'resources' => [
              {
                'guid' => buildpack3.guid,
                'created_at' => iso8601,
                'updated_at' => iso8601,
                'name' => buildpack3.name,
                'state' => buildpack3.state,
                'filename' => buildpack3.filename,
                'stack' => buildpack3.stack,
                'position' => 2,
                'enabled' => true,
                'locked' => false,
                'metadata' => { 'labels' => {}, 'annotations' => {} },
                'links' => {
                  'self' => {
                    'href' => "#{link_prefix}/v3/buildpacks/#{buildpack3.guid}"
                  },
                  'upload' => {
                    'href' => "#{link_prefix}/v3/buildpacks/#{buildpack3.guid}/upload",
                    'method' => 'POST'
                  }
                }
              },
              {
                'guid' => buildpack1.guid,
                'created_at' => iso8601,
                'updated_at' => iso8601,
                'name' => buildpack1.name,
                'state' => buildpack1.state,
                'filename' => buildpack1.filename,
                'stack' => buildpack1.stack,
                'position' => 1,
                'enabled' => true,
                'locked' => false,
                'metadata' => { 'labels' => {}, 'annotations' => {} },
                'links' => {
                  'self' => {
                    'href' => "#{link_prefix}/v3/buildpacks/#{buildpack1.guid}"
                  },
                  'upload' => {
                    'href' => "#{link_prefix}/v3/buildpacks/#{buildpack1.guid}/upload",
                    'method' => 'POST'
                  }
                }
              }
            ]
          }
        )
      end
    end

    context 'permissions' do
      let(:org) { VCAP::CloudController::Organization.make }
      let(:space) { VCAP::CloudController::Space.make(organization: org) }
      let(:api_call) { ->(user_headers) { get '/v3/buildpacks', nil, user_headers } }
      let(:expected_codes_and_responses) { Hash.new(code: 200) }

      before do
        space.organization.add_user(user)
      end

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

  describe 'POST /v3/buildpacks' do
    context 'when not authenticated' do
      let(:headers) { {} }

      it 'returns 401' do
        post '/v3/buildpacks', nil, headers

        expect(last_response.status).to eq(401)
      end
    end

    context 'when authenticated but not admin' do
      let(:user) { VCAP::CloudController::User.make }
      let(:headers) { headers_for(user) }

      it 'returns 403' do
        params = {}

        post '/v3/buildpacks', params, headers

        expect(last_response.status).to eq(403)
      end
    end

    context 'when authenticated and admin' do
      let(:user) { VCAP::CloudController::User.make }
      let(:headers) { admin_headers_for(user) }

      context 'when successful' do
        let(:stack) { VCAP::CloudController::Stack.make }
        let(:params) do
          {
            name: 'the-r3al_Name',
            stack: stack.name,
            enabled: false,
            locked: true,
            metadata: {
              labels: {
                potato: 'yam'
              },
              annotations: {
                potato: 'idaho'
              }
            }
          }
        end

        it 'returns 201' do
          post '/v3/buildpacks', params.to_json, headers

          expect(last_response.status).to eq(201)
        end

        describe 'non-position values' do
          it 'returns the newly-created buildpack resource' do
            post '/v3/buildpacks', params.to_json, headers

            buildpack = VCAP::CloudController::Buildpack.last

            expected_response = {
              'name' => params[:name],
              'state' => 'AWAITING_UPLOAD',
              'filename' => nil,
              'stack' => params[:stack],
              'position' => 1,
              'enabled' => params[:enabled],
              'locked' => params[:locked],
              'guid' => buildpack.guid,
              'created_at' => iso8601,
              'updated_at' => iso8601,
              'metadata' => {
                'labels' => {
                  'potato' => 'yam'
                },
                'annotations' => {
                  'potato' => 'idaho'
                }
              },
              'links' => {
                'self' => {
                  'href' => "#{link_prefix}/v3/buildpacks/#{buildpack.guid}"
                },
                'upload' => {
                  'href' => "#{link_prefix}/v3/buildpacks/#{buildpack.guid}/upload",
                  'method' => 'POST'
                }
              }
            }
            expect(parsed_response).to be_a_response_like(expected_response)
          end
        end

        describe 'position' do
          let!(:buildpack1) { VCAP::CloudController::Buildpack.make(position: 1) }
          let!(:buildpack2) { VCAP::CloudController::Buildpack.make(position: 2) }
          let!(:buildpack3) { VCAP::CloudController::Buildpack.make(position: 3) }

          context 'the position is not provided' do
            it 'defaults the position value to 1' do
              post '/v3/buildpacks', params.to_json, headers

              expect(parsed_response['position']).to eq(1)
              expect(buildpack1.reload.position).to eq(2)
              expect(buildpack2.reload.position).to eq(3)
              expect(buildpack3.reload.position).to eq(4)
            end
          end

          context 'the position is less than or equal to the total number of buildpacks' do
            before do
              params[:position] = 2
            end

            it 'sets the position value to the provided position' do
              post '/v3/buildpacks', params.to_json, headers

              expect(parsed_response['position']).to eq(2)
              expect(buildpack1.reload.position).to eq(1)
              expect(buildpack2.reload.position).to eq(3)
              expect(buildpack3.reload.position).to eq(4)
            end
          end

          context 'the position is greater than the total number of buildpacks' do
            before do
              params[:position] = 42
            end

            it 'sets the position value to the provided position' do
              post '/v3/buildpacks', params.to_json, headers

              expect(parsed_response['position']).to eq(4)
              expect(buildpack1.reload.position).to eq(1)
              expect(buildpack2.reload.position).to eq(2)
              expect(buildpack3.reload.position).to eq(3)
            end
          end
        end
      end
    end
  end

  describe 'GET /v3/buildpacks/:guid' do
    let(:params) { {} }
    let(:buildpack) { VCAP::CloudController::Buildpack.make }

    context 'when not authenticated' do
      it 'returns 401' do
        headers = {}

        get "/v3/buildpacks/#{buildpack.guid}", params, headers

        expect(last_response.status).to eq(401)
      end
    end

    context 'when authenticated' do
      let(:org) { VCAP::CloudController::Organization.make }
      let(:space) { VCAP::CloudController::Space.make(organization: org) }
      let(:user) { VCAP::CloudController::User.make }

      before do
        space.organization.add_user(user)
      end

      context 'the buildpack does not exist' do
        let(:api_call) { ->(user_headers) { get '/v3/buildpacks/does-not-exist', nil, user_headers } }

        let(:expected_codes_and_responses) { Hash.new(code: 404) }

        it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
      end

      context 'the buildpack exists' do
        let(:api_call) { ->(user_headers) { get "/v3/buildpacks/#{buildpack.guid}", nil, user_headers } }
        let(:buildpack_response) do
          {
            'name' => buildpack.name,
            'state' => buildpack.state,
            'stack' => buildpack.stack,
            'filename' => buildpack.filename,
            'position' => buildpack.position,
            'enabled' => buildpack.enabled,
            'locked' => buildpack.locked,
            'guid' => buildpack.guid,
            'created_at' => iso8601,
            'updated_at' => iso8601,
            'metadata' => { 'labels' => {}, 'annotations' => {} },
            'links' => {
              'self' => {
                'href' => "#{link_prefix}/v3/buildpacks/#{buildpack.guid}"
              },
              'upload' => {
                'href' => "#{link_prefix}/v3/buildpacks/#{buildpack.guid}/upload",
                'method' => 'POST'
              }
            }
          }
        end

        let(:expected_codes_and_responses) { Hash.new(code: 200, response_object: buildpack_response) }

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

  describe 'DELETE /v3/buildpacks/:guid' do
    let(:buildpack) { VCAP::CloudController::Buildpack.make }

    it 'deletes a buildpack asynchronously' do
      delete "/v3/buildpacks/#{buildpack.guid}", nil, admin_headers

      expect(last_response.status).to eq(202)
      expect(last_response.headers['Location']).to match(%r{http.+/v3/jobs/[a-fA-F0-9-]+})

      execute_all_jobs(expected_successes: 2, expected_failures: 0)
      get "/v3/buildpacks/#{buildpack.guid}", {}, admin_headers
      expect(last_response.status).to eq(404)
    end

    context 'deleting metadata' do
      it_behaves_like 'resource with metadata' do
        let(:resource) { buildpack }
        let(:api_call) do
          -> { delete "/v3/buildpacks/#{resource.guid}", nil, admin_headers }
        end
      end
    end
  end

  describe 'POST /v3/buildpacks/:guid/upload' do
    let(:buildpack) { VCAP::CloudController::Buildpack.make }

    before do
      allow_any_instance_of(VCAP::CloudController::BuildpackUploadMessage).to receive(:valid?).and_return(true)
    end

    it 'enqueues a job to process the uploaded bits' do
      file_upload_params = {
        bits_name: 'buildpack.zip',
        bits_path: 'tmpdir/buildpack.zip'
      }

      expect(Delayed::Job.count).to eq 0

      post "/v3/buildpacks/#{buildpack.guid}/upload", file_upload_params.to_json, admin_headers

      expect(Delayed::Job.count).to eq 1

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

      get last_response.headers['Location'], nil, admin_headers

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

  describe 'PATCH /v3/buildpacks/:guid' do
    let(:buildpack) { VCAP::CloudController::Buildpack.make }

    it 'updates a buildpack' do
      params = { enabled: false }

      patch "/v3/buildpacks/#{buildpack.guid}", params.to_json, admin_headers

      expect(parsed_response['enabled']).to be(false)
      expect(last_response.status).to eq(200)
      expect(buildpack.reload).not_to be_enabled
    end
  end
end