cloudfoundry/cloud_controller_ng

View on GitHub
spec/request/deployments_spec.rb

Summary

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

RSpec.describe 'Deployments' do
  let(:user) { VCAP::CloudController::User.make }
  let(:space) { app_model.space }
  let(:org) { space.organization }
  let(:app_model) { VCAP::CloudController::AppModel.make(desired_state: VCAP::CloudController::ProcessModel::STARTED) }
  let(:droplet) { VCAP::CloudController::DropletModel.make(app: app_model, process_types: { web: 'webby' }) }
  let!(:process_model) { VCAP::CloudController::ProcessModel.make(app: app_model) }
  let(:admin_header) { headers_for(user, scopes: %w[cloud_controller.admin]) }
  let(:user_header) { headers_for(user, email: user_email, user_name: user_name) }
  let(:user_email) { Sham.email }
  let(:user_name) { 'some-username' }
  let(:metadata) { { 'labels' => {}, 'annotations' => {} } }

  before do
    TestConfig.override(temporary_disable_deployments: false)
    app_model.update(droplet_guid: droplet.guid)
  end

  describe 'POST /v3/deployments' do
    context 'when a droplet is not supplied with the request' do
      let(:create_request) do
        {
          relationships: {
            app: {
              data: {
                guid: app_model.guid
              }
            }
          }
        }
      end
      let(:expected_response) do
        {
          'guid' => UUID_REGEX,
          'status' => {
            'value' => VCAP::CloudController::DeploymentModel::ACTIVE_STATUS_VALUE,
            'reason' => VCAP::CloudController::DeploymentModel::DEPLOYING_STATUS_REASON,
            'details' => {
              'last_successful_healthcheck' => iso8601
            }
          },
          'strategy' => 'rolling',
          'droplet' => {
            'guid' => droplet.guid
          },
          'revision' => {
            'guid' => UUID_REGEX,
            'version' => 1
          },
          'previous_droplet' => {
            'guid' => droplet.guid
          },
          'new_processes' => [{
            'guid' => UUID_REGEX,
            'type' => 'web'
          }],
          'created_at' => iso8601,
          'updated_at' => iso8601,
          'metadata' => metadata,
          'relationships' => {
            'app' => {
              'data' => {
                'guid' => app_model.guid
              }
            }
          },
          'links' => {
            'self' => {
              'href' => %r{#{link_prefix}/v3/deployments/#{UUID_REGEX}}
            },
            'app' => {
              'href' => "#{link_prefix}/v3/apps/#{app_model.guid}"
            },
            'cancel' => {
              'href' => %r{#{link_prefix}/v3/deployments/#{UUID_REGEX}/actions/cancel},
              'method' => 'POST'
            }
          }
        }
      end
      let(:api_call) { ->(user_headers) { post '/v3/deployments', create_request.to_json, user_headers } }
      let(:expected_codes_and_responses) do
        h = Hash.new(code: 422)
        h['admin'] = h['space_developer'] = h['space_supporter'] = { code: 201, response_object: expected_response }
        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: 422 } }
          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 a droplet is supplied with the request' do
      let(:user) { make_developer_for_space(space) }
      let(:other_droplet) { VCAP::CloudController::DropletModel.make(app: app_model, process_types: { web: 'start-me-up' }) }
      let(:create_request) do
        {
          droplet: {
            guid: other_droplet.guid
          },
          relationships: {
            app: {
              data: {
                guid: app_model.guid
              }
            }
          }
        }
      end

      it 'creates a deployment object with that droplet' do
        post '/v3/deployments', create_request.to_json, user_header
        expect(last_response.status).to eq(201)
        parsed_response = Oj.load(last_response.body)

        deployment = VCAP::CloudController::DeploymentModel.last

        expect(parsed_response).to be_a_response_like({
                                                        'guid' => deployment.guid,
                                                        'status' => {
                                                          'value' => VCAP::CloudController::DeploymentModel::ACTIVE_STATUS_VALUE,
                                                          'reason' => VCAP::CloudController::DeploymentModel::DEPLOYING_STATUS_REASON,
                                                          'details' => {
                                                            'last_successful_healthcheck' => iso8601
                                                          }
                                                        },
                                                        'strategy' => 'rolling',
                                                        'droplet' => {
                                                          'guid' => other_droplet.guid
                                                        },
                                                        'revision' => {
                                                          'guid' => app_model.latest_revision.guid,
                                                          'version' => app_model.latest_revision.version
                                                        },
                                                        'previous_droplet' => {
                                                          'guid' => droplet.guid
                                                        },
                                                        'new_processes' => [{
                                                          'guid' => deployment.deploying_web_process.guid,
                                                          'type' => deployment.deploying_web_process.type
                                                        }],
                                                        'created_at' => iso8601,
                                                        'updated_at' => iso8601,
                                                        'metadata' => metadata,
                                                        'relationships' => {
                                                          'app' => {
                                                            'data' => {
                                                              'guid' => app_model.guid
                                                            }
                                                          }
                                                        },
                                                        'links' => {
                                                          'self' => {
                                                            'href' => "#{link_prefix}/v3/deployments/#{deployment.guid}"
                                                          },
                                                          'app' => {
                                                            'href' => "#{link_prefix}/v3/apps/#{app_model.guid}"
                                                          },
                                                          'cancel' => {
                                                            'href' => "#{link_prefix}/v3/deployments/#{deployment.guid}/actions/cancel",
                                                            'method' => 'POST'
                                                          }
                                                        }
                                                      })
      end
    end

    context 'when a revision is supplied with the request' do
      let(:user) { make_developer_for_space(space) }
      let(:other_droplet) { VCAP::CloudController::DropletModel.make(app: app_model, process_types: { web: 'webby' }) }
      let!(:revision) { VCAP::CloudController::RevisionModel.make(app: app_model, droplet: other_droplet, created_at: 5.days.ago) }
      let!(:revision2) { VCAP::CloudController::RevisionModel.make(app: app_model, droplet: droplet) }

      let(:create_request) do
        {
          revision: {
            guid: revision.guid
          },
          relationships: {
            app: {
              data: {
                guid: app_model.guid
              }
            }
          }
        }
      end

      it 'creates a deployment object using the droplet associated with the revision' do
        revision_count = VCAP::CloudController::RevisionModel.count
        post '/v3/deployments', create_request.to_json, user_header
        expect(last_response.status).to eq(201), last_response.body
        expect(VCAP::CloudController::RevisionModel.count).to eq(revision_count + 1)

        parsed_response = Oj.load(last_response.body)

        deployment = VCAP::CloudController::DeploymentModel.last
        revision = VCAP::CloudController::RevisionModel.last

        expect(parsed_response).to be_a_response_like({
                                                        'guid' => deployment.guid,
                                                        'status' => {
                                                          'value' => VCAP::CloudController::DeploymentModel::ACTIVE_STATUS_VALUE,
                                                          'reason' => VCAP::CloudController::DeploymentModel::DEPLOYING_STATUS_REASON,
                                                          'details' => {
                                                            'last_successful_healthcheck' => iso8601
                                                          }
                                                        },
                                                        'strategy' => 'rolling',
                                                        'droplet' => {
                                                          'guid' => other_droplet.guid
                                                        },
                                                        'revision' => {
                                                          'guid' => revision.guid,
                                                          'version' => revision.version
                                                        },
                                                        'previous_droplet' => {
                                                          'guid' => droplet.guid
                                                        },
                                                        'new_processes' => [{
                                                          'guid' => deployment.deploying_web_process.guid,
                                                          'type' => deployment.deploying_web_process.type
                                                        }],
                                                        'created_at' => iso8601,
                                                        'updated_at' => iso8601,
                                                        'metadata' => metadata,
                                                        'relationships' => {
                                                          'app' => {
                                                            'data' => {
                                                              'guid' => app_model.guid
                                                            }
                                                          }
                                                        },
                                                        'links' => {
                                                          'self' => {
                                                            'href' => "#{link_prefix}/v3/deployments/#{deployment.guid}"
                                                          },
                                                          'app' => {
                                                            'href' => "#{link_prefix}/v3/apps/#{app_model.guid}"
                                                          },
                                                          'cancel' => {
                                                            'href' => "#{link_prefix}/v3/deployments/#{deployment.guid}/actions/cancel",
                                                            'method' => 'POST'
                                                          }
                                                        }
                                                      })
      end
    end

    context 'when a revision AND a droplet are supplied with the request' do
      let(:create_request) do
        {
          revision: {
            guid: 'bar'
          },
          droplet: {
            guid: 'foo'
          },
          relationships: {
            app: {
              data: {
                guid: app_model.guid
              }
            }
          }
        }
      end

      it 'fails' do
        post '/v3/deployments', create_request.to_json, user_header
        expect(last_response.status).to eq(422)

        parsed_response = Oj.load(last_response.body)
        expect(parsed_response['errors'][0]['detail']).to match('Cannot set both fields')
      end
    end

    context 'when metadata is supplied with the request' do
      let(:metadata) do
        {
          'labels' => {
            release: 'stable',
            'seriouseats.com/potato' => 'mashed'
          },
          'annotations' => {
            potato: 'idaho'
          }
        }
      end
      let(:user) { make_developer_for_space(space) }

      let(:create_request) do
        {
          relationships: {
            app: {
              data: {
                guid: app_model.guid
              }
            }
          },
          metadata: metadata
        }
      end

      it 'creates a deployment object with the metadata' do
        post '/v3/deployments', create_request.to_json, user_header
        expect(last_response.status).to eq(201)

        deployment = VCAP::CloudController::DeploymentModel.last
        expect(deployment).to have_labels(
          { prefix: 'seriouseats.com', key_name: 'potato', value: 'mashed' },
          { prefix: nil, key_name: 'release', value: 'stable' }
        )
        expect(deployment).to have_annotations(
          { key_name: 'potato', value: 'idaho' }
        )

        expect(parsed_response).to be_a_response_like({
                                                        'guid' => deployment.guid,
                                                        'status' => {
                                                          'value' => VCAP::CloudController::DeploymentModel::ACTIVE_STATUS_VALUE,
                                                          'reason' => VCAP::CloudController::DeploymentModel::DEPLOYING_STATUS_REASON,
                                                          'details' => {
                                                            'last_successful_healthcheck' => iso8601
                                                          }
                                                        },
                                                        'strategy' => 'rolling',
                                                        'droplet' => {
                                                          'guid' => droplet.guid
                                                        },
                                                        'revision' => {
                                                          'guid' => app_model.latest_revision.guid,
                                                          'version' => app_model.latest_revision.version
                                                        },
                                                        'previous_droplet' => {
                                                          'guid' => droplet.guid
                                                        },
                                                        'new_processes' => [{
                                                          'guid' => deployment.deploying_web_process.guid,
                                                          'type' => deployment.deploying_web_process.type
                                                        }],
                                                        'metadata' => { 'labels' => { 'release' => 'stable', 'seriouseats.com/potato' => 'mashed' },
                                                                        'annotations' => { 'potato' => 'idaho' } },
                                                        'created_at' => iso8601,
                                                        'updated_at' => iso8601,
                                                        'relationships' => {
                                                          'app' => {
                                                            'data' => {
                                                              'guid' => app_model.guid
                                                            }
                                                          }
                                                        },
                                                        'links' => {
                                                          'self' => {
                                                            'href' => "#{link_prefix}/v3/deployments/#{deployment.guid}"
                                                          },
                                                          'app' => {
                                                            'href' => "#{link_prefix}/v3/apps/#{app_model.guid}"
                                                          },
                                                          'cancel' => {
                                                            'href' => "#{link_prefix}/v3/deployments/#{deployment.guid}/actions/cancel",
                                                            'method' => 'POST'
                                                          }
                                                        }
                                                      })
      end
    end

    context 'when revisions are enabled' do
      let(:user) { make_developer_for_space(space) }
      let(:other_droplet) { VCAP::CloudController::DropletModel.make(app: app_model, process_types: { web: 'start-me-up' }) }
      let(:create_request) do
        {
          droplet: {
            guid: other_droplet.guid
          },
          relationships: {
            app: {
              data: {
                guid: app_model.guid
              }
            }
          }
        }
      end

      it 'creates a deployment with a reference to the new revision' do
        expect do
          post '/v3/deployments', create_request.to_json, user_header
          expect(last_response.status).to eq(201), last_response.body
        end.to change(VCAP::CloudController::RevisionModel, :count).by(1)

        deployment = VCAP::CloudController::DeploymentModel.last
        revision = VCAP::CloudController::RevisionModel.last
        parsed_response = Oj.load(last_response.body)
        expect(parsed_response).to be_a_response_like({
                                                        'guid' => deployment.guid,
                                                        'status' => {
                                                          'value' => VCAP::CloudController::DeploymentModel::ACTIVE_STATUS_VALUE,
                                                          'reason' => VCAP::CloudController::DeploymentModel::DEPLOYING_STATUS_REASON,
                                                          'details' => {
                                                            'last_successful_healthcheck' => iso8601
                                                          }
                                                        },
                                                        'strategy' => 'rolling',
                                                        'droplet' => {
                                                          'guid' => other_droplet.guid
                                                        },
                                                        'revision' => {
                                                          'guid' => revision.guid,
                                                          'version' => revision.version
                                                        },
                                                        'previous_droplet' => {
                                                          'guid' => droplet.guid
                                                        },
                                                        'new_processes' => [{
                                                          'guid' => deployment.deploying_web_process.guid,
                                                          'type' => deployment.deploying_web_process.type
                                                        }],
                                                        'created_at' => iso8601,
                                                        'updated_at' => iso8601,
                                                        'metadata' => metadata,
                                                        'relationships' => {
                                                          'app' => {
                                                            'data' => {
                                                              'guid' => app_model.guid
                                                            }
                                                          }
                                                        },
                                                        'links' => {
                                                          'self' => {
                                                            'href' => "#{link_prefix}/v3/deployments/#{deployment.guid}"
                                                          },
                                                          'app' => {
                                                            'href' => "#{link_prefix}/v3/apps/#{app_model.guid}"
                                                          },
                                                          'cancel' => {
                                                            'href' => "#{link_prefix}/v3/deployments/#{deployment.guid}/actions/cancel",
                                                            'method' => 'POST'
                                                          }
                                                        }
                                                      })
      end
    end

    context 'when the app is stopped' do
      let(:user) { make_developer_for_space(space) }
      let(:other_droplet) { VCAP::CloudController::DropletModel.make(app: app_model, process_types: { web: 'start-me-up' }) }
      let(:create_request) do
        {
          droplet: {
            guid: other_droplet.guid
          },
          relationships: {
            app: {
              data: {
                guid: app_model.guid
              }
            }
          }
        }
      end

      before do
        app_model.update(desired_state: VCAP::CloudController::ProcessModel::STOPPED)
        app_model.save
      end

      it 'creates a deployment object in state DEPLOYED' do
        post '/v3/deployments', create_request.to_json, user_header
        expect(last_response.status).to eq(201)
        parsed_response = Oj.load(last_response.body)

        deployment = VCAP::CloudController::DeploymentModel.last

        expect(parsed_response).to be_a_response_like({
                                                        'guid' => deployment.guid,
                                                        'status' => {
                                                          'value' => VCAP::CloudController::DeploymentModel::FINALIZED_STATUS_VALUE,
                                                          'reason' => VCAP::CloudController::DeploymentModel::DEPLOYED_STATUS_REASON,
                                                          'details' => {
                                                            'last_successful_healthcheck' => iso8601
                                                          }
                                                        },
                                                        'strategy' => 'rolling',
                                                        'droplet' => {
                                                          'guid' => other_droplet.guid
                                                        },
                                                        'revision' => {
                                                          'guid' => app_model.latest_revision.guid,
                                                          'version' => app_model.latest_revision.version
                                                        },
                                                        'previous_droplet' => {
                                                          'guid' => droplet.guid
                                                        },
                                                        'new_processes' => [],
                                                        'created_at' => iso8601,
                                                        'updated_at' => iso8601,
                                                        'metadata' => metadata,
                                                        'relationships' => {
                                                          'app' => {
                                                            'data' => {
                                                              'guid' => app_model.guid
                                                            }
                                                          }
                                                        },
                                                        'links' => {
                                                          'self' => {
                                                            'href' => "#{link_prefix}/v3/deployments/#{deployment.guid}"
                                                          },
                                                          'app' => {
                                                            'href' => "#{link_prefix}/v3/apps/#{app_model.guid}"
                                                          }
                                                        }
                                                      })
      end

      it 'starts the app' do
        post '/v3/deployments', create_request.to_json, user_header
        expect(last_response.status).to eq(201)

        expect(app_model.reload.desired_state).to eq(VCAP::CloudController::ProcessModel::STARTED)
      end

      context 'when "strategy":"rolling" is provided' do
        it 'starts the app' do
          post '/v3/deployments', create_request.merge({ strategy: 'rolling' }).to_json, user_header
          expect(last_response.status).to eq(201)

          expect(app_model.reload.desired_state).to eq(VCAP::CloudController::ProcessModel::STARTED)
        end
      end
    end

    context 'telemetry' do
      let!(:other_droplet) { VCAP::CloudController::DropletModel.make(app: app_model, process_types: { web: 'webboo' }) }
      let!(:revision) { VCAP::CloudController::RevisionModel.make(app: app_model, droplet: other_droplet, created_at: 5.days.ago) }
      let!(:revision2) { VCAP::CloudController::RevisionModel.make(app: app_model, droplet: droplet) }
      let(:user) { make_developer_for_space(space) }

      let(:create_request) do
        {
          relationships: {
            app: {
              data: {
                guid: app_model.guid
              }
            }
          }
        }
      end
      let(:revision_create_request) do
        {
          revision: {
            guid: revision.guid
          },
          relationships: {
            app: {
              data: {
                guid: app_model.guid
              }
            }
          }
        }
      end

      it 'logs the required fields when a deployment is created' do
        Timecop.freeze do
          expected_json = {
            'telemetry-source' => 'cloud_controller_ng',
            'telemetry-time' => Time.now.to_datetime.rfc3339,
            'create-deployment' => {
              'api-version' => 'v3',
              'strategy' => 'rolling',
              'app-id' => OpenSSL::Digest::SHA256.hexdigest(app_model.guid),
              'user-id' => OpenSSL::Digest::SHA256.hexdigest(user.guid)
            }
          }
          expect_any_instance_of(ActiveSupport::Logger).to receive(:info).with(Oj.dump(expected_json))

          post '/v3/deployments', create_request.to_json, user_header
          expect(last_response.status).to eq(201), last_response.body
        end
      end

      it 'logs the roll back app request' do
        app_model.update(revisions_enabled: true)
        Timecop.freeze do
          expected_json = {
            'telemetry-source' => 'cloud_controller_ng',
            'telemetry-time' => Time.now.to_datetime.rfc3339,
            'rolled-back-app' => {
              'api-version' => 'v3',
              'strategy' => 'rolling',
              'app-id' => OpenSSL::Digest::SHA256.hexdigest(app_model.guid),
              'user-id' => OpenSSL::Digest::SHA256.hexdigest(user.guid),
              'revision-id' => OpenSSL::Digest::SHA256.hexdigest(revision.guid)
            }
          }
          expect_any_instance_of(ActiveSupport::Logger).to receive(:info).twice
          expect_any_instance_of(ActiveSupport::Logger).to receive(:info).with(Oj.dump(expected_json)).at_most(:once)

          post '/v3/deployments', revision_create_request.to_json, user_header
          expect(last_response.status).to eq(201), last_response.body
        end
      end
    end

    context 'strategy' do
      let(:create_request) do
        {
          strategy: strategy,
          relationships: {
            app: {
              data: {
                guid: app_model.guid
              }
            }
          }
        }
      end

      context 'when no strategy is provided' do
        let(:user) { make_developer_for_space(space) }
        let(:create_request) do
          {
            relationships: {
              app: {
                data: {
                  guid: app_model.guid
                }
              }
            }
          }
        end

        it 'creates a deployment with strategy "rolling"' do
          post '/v3/deployments', create_request.to_json, user_header
          expect(last_response.status).to eq(201)

          deployment = VCAP::CloudController::DeploymentModel.last

          expect(parsed_response).to be_a_response_like({
                                                          'guid' => deployment.guid,
                                                          'status' => {
                                                            'value' => VCAP::CloudController::DeploymentModel::ACTIVE_STATUS_VALUE,
                                                            'reason' => VCAP::CloudController::DeploymentModel::DEPLOYING_STATUS_REASON,
                                                            'details' => {
                                                              'last_successful_healthcheck' => iso8601
                                                            }
                                                          },
                                                          'strategy' => 'rolling',
                                                          'droplet' => {
                                                            'guid' => droplet.guid
                                                          },
                                                          'revision' => {
                                                            'guid' => app_model.latest_revision.guid,
                                                            'version' => app_model.latest_revision.version
                                                          },
                                                          'previous_droplet' => {
                                                            'guid' => droplet.guid
                                                          },
                                                          'new_processes' => [{
                                                            'guid' => deployment.deploying_web_process.guid,
                                                            'type' => deployment.deploying_web_process.type
                                                          }],
                                                          'created_at' => iso8601,
                                                          'updated_at' => iso8601,
                                                          'metadata' => metadata,
                                                          'relationships' => {
                                                            'app' => {
                                                              'data' => {
                                                                'guid' => app_model.guid
                                                              }
                                                            }
                                                          },
                                                          'links' => {
                                                            'self' => {
                                                              'href' => "#{link_prefix}/v3/deployments/#{deployment.guid}"
                                                            },
                                                            'app' => {
                                                              'href' => "#{link_prefix}/v3/apps/#{app_model.guid}"
                                                            },
                                                            'cancel' => {
                                                              'href' => "#{link_prefix}/v3/deployments/#{deployment.guid}/actions/cancel",
                                                              'method' => 'POST'
                                                            }
                                                          }
                                                        })
        end
      end

      context 'when strategy "rolling" is provided' do
        let(:strategy) { 'rolling' }
        let(:user) { make_developer_for_space(space) }

        it 'creates a deployment with strategy "rolling" when "strategy":"rolling" is provided' do
          post '/v3/deployments', create_request.to_json, user_header
          expect(last_response.status).to eq(201), last_response.body

          deployment = VCAP::CloudController::DeploymentModel.last

          expect(parsed_response).to be_a_response_like({
                                                          'guid' => deployment.guid,
                                                          'status' => {
                                                            'value' => VCAP::CloudController::DeploymentModel::ACTIVE_STATUS_VALUE,
                                                            'reason' => VCAP::CloudController::DeploymentModel::DEPLOYING_STATUS_REASON,
                                                            'details' => {
                                                              'last_successful_healthcheck' => iso8601
                                                            }
                                                          },
                                                          'strategy' => 'rolling',
                                                          'droplet' => {
                                                            'guid' => droplet.guid
                                                          },
                                                          'revision' => {
                                                            'guid' => app_model.latest_revision.guid,
                                                            'version' => app_model.latest_revision.version
                                                          },
                                                          'previous_droplet' => {
                                                            'guid' => droplet.guid
                                                          },
                                                          'new_processes' => [{
                                                            'guid' => deployment.deploying_web_process.guid,
                                                            'type' => deployment.deploying_web_process.type
                                                          }],
                                                          'created_at' => iso8601,
                                                          'updated_at' => iso8601,
                                                          'metadata' => metadata,
                                                          'relationships' => {
                                                            'app' => {
                                                              'data' => {
                                                                'guid' => app_model.guid
                                                              }
                                                            }
                                                          },
                                                          'links' => {
                                                            'self' => {
                                                              'href' => "#{link_prefix}/v3/deployments/#{deployment.guid}"
                                                            },
                                                            'app' => {
                                                              'href' => "#{link_prefix}/v3/apps/#{app_model.guid}"
                                                            },
                                                            'cancel' => {
                                                              'href' => "#{link_prefix}/v3/deployments/#{deployment.guid}/actions/cancel",
                                                              'method' => 'POST'
                                                            }
                                                          }
                                                        })
        end
      end

      context 'when a strategy other than "rolling" is provided' do
        let(:strategy) { 'potato' }

        it 'returns a 422 and error' do
          post '/v3/deployments', create_request.to_json, user_header
          expect(last_response.status).to eq(422)

          parsed_response = Oj.load(last_response.body)
          expect(parsed_response['errors'][0]['detail']).to match("Strategy 'potato' is not a supported deployment strategy")
        end
      end
    end

    context 'validation failures' do
      let(:user) { make_developer_for_space(space) }
      let(:smol_quota) { VCAP::CloudController::QuotaDefinition.make(memory_limit: 1) }
      let(:create_request) do
        {
          relationships: {
            app: {
              data: {
                guid: app_model.guid
              }
            }
          }
        }
      end

      before do
        org.quota_definition = smol_quota
        org.save
      end

      it 'returns a 422 when a quota is violated' do
        post '/v3/deployments', create_request.to_json, user_header
        expect(last_response.status).to eq(422)

        expect(parsed_response['errors'][0]['detail']).to match('memory quota_exceeded')
      end
    end
  end

  describe 'PATCH /v3/deployments/:guid' do
    let(:user) { make_developer_for_space(space) }
    let(:deployment) do
      VCAP::CloudController::DeploymentModel.make(
        app: app_model,
        droplet: droplet
      )
    end
    let(:update_request) do
      {
        metadata: {
          labels: {
            freaky: 'thursday'
          },
          annotations: {
            quality: 'p sus'
          }
        }
      }.to_json
    end

    it 'updates the deployment with metadata' do
      patch "/v3/deployments/#{deployment.guid}", update_request, user_header
      expect(last_response.status).to eq(200)

      parsed_response = Oj.load(last_response.body)
      expect(parsed_response).to be_a_response_like({
                                                      'guid' => deployment.guid,
                                                      'status' => {
                                                        'value' => VCAP::CloudController::DeploymentModel::ACTIVE_STATUS_VALUE,
                                                        'reason' => VCAP::CloudController::DeploymentModel::DEPLOYING_STATUS_REASON,
                                                        'details' => {
                                                          'last_successful_healthcheck' => iso8601
                                                        }
                                                      },
                                                      'strategy' => 'rolling',
                                                      'droplet' => {
                                                        'guid' => droplet.guid
                                                      },
                                                      'revision' => nil,
                                                      'previous_droplet' => {
                                                        'guid' => nil
                                                      },
                                                      'new_processes' => [],
                                                      'metadata' => {
                                                        'labels' => { 'freaky' => 'thursday' },
                                                        'annotations' => { 'quality' => 'p sus' }
                                                      },
                                                      'created_at' => iso8601,
                                                      'updated_at' => iso8601,
                                                      'relationships' => {
                                                        'app' => {
                                                          'data' => {
                                                            'guid' => app_model.guid
                                                          }
                                                        }
                                                      },
                                                      'links' => {
                                                        'self' => {
                                                          'href' => "#{link_prefix}/v3/deployments/#{deployment.guid}"
                                                        },
                                                        'app' => {
                                                          'href' => "#{link_prefix}/v3/apps/#{app_model.guid}"
                                                        },
                                                        'cancel' => {
                                                          'href' => "#{link_prefix}/v3/deployments/#{deployment.guid}/actions/cancel",
                                                          'method' => 'POST'
                                                        }
                                                      }
                                                    })
    end

    context 'permissions' do
      before do
        space.remove_developer(user)
      end

      let(:api_call) { ->(user_headers) { patch "/v3/deployments/#{deployment.guid}", update_request, user_headers } }

      let(:expected_codes_and_responses) do
        h = Hash.new(code: 403, errors: CF_NOT_AUTHORIZED)
        h['admin'] = { code: 200 }
        h['space_developer'] = { code: 200 }
        %w[org_auditor org_billing_manager no_role].each { |r| h[r] = { 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()
          h['space_developer'] = { code: 403, errors: CF_ORG_SUSPENDED }
          h
        end

        before do
          org.update(status: VCAP::CloudController::Organization::SUSPENDED)
        end

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

  describe 'GET /v3/deployments/:guid' do
    let(:api_call) { ->(user_headers) { get "/v3/deployments/#{deployment.guid}", nil, user_headers } }
    let(:old_droplet) { VCAP::CloudController::DropletModel.make }
    let(:deployment) do
      VCAP::CloudController::DeploymentModelTestFactory.make(
        app: app_model,
        droplet: droplet,
        previous_droplet: old_droplet
      )
    end
    let(:expected_response) do
      {
        'guid' => deployment.guid,
        'status' => {
          'value' => VCAP::CloudController::DeploymentModel::ACTIVE_STATUS_VALUE,
          'reason' => VCAP::CloudController::DeploymentModel::DEPLOYING_STATUS_REASON,
          'details' => {
            'last_successful_healthcheck' => iso8601
          }
        },
        'droplet' => {
          'guid' => droplet.guid
        },
        'revision' => nil,
        'previous_droplet' => {
          'guid' => old_droplet.guid
        },
        'new_processes' => [{
          'guid' => deployment.deploying_web_process.guid,
          'type' => deployment.deploying_web_process.type
        }],
        'created_at' => iso8601,
        'updated_at' => iso8601,
        'metadata' => metadata,
        'strategy' => 'rolling',
        'relationships' => {
          'app' => {
            'data' => {
              'guid' => app_model.guid
            }
          }
        },
        'links' => {
          'self' => {
            'href' => "#{link_prefix}/v3/deployments/#{deployment.guid}"
          },
          'app' => {
            'href' => "#{link_prefix}/v3/apps/#{app_model.guid}"
          },
          'cancel' => {
            'href' => "#{link_prefix}/v3/deployments/#{deployment.guid}/actions/cancel",
            'method' => 'POST'
          }
        }
      }
    end
    let(:expected_codes_and_responses) do
      h = Hash.new(code: 200, response_object: expected_response)
      h['org_auditor'] = h['org_billing_manager'] = h['no_role'] = { code: 404 }
      h
    end

    it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
  end

  describe 'GET /v3/deployments/' do
    let(:user) {  VCAP::CloudController::User.make }
    let(:space) { app_model.space }
    let(:app_model) { droplet.app }
    let(:droplet) { VCAP::CloudController::DropletModel.make(guid: 'droplet1') }
    let!(:deployment) do
      VCAP::CloudController::DeploymentModelTestFactory.make(
        app: app_model,
        droplet: app_model.droplet,
        previous_droplet: app_model.droplet,
        status_value: VCAP::CloudController::DeploymentModel::ACTIVE_STATUS_VALUE,
        state: VCAP::CloudController::DeploymentModel::DEPLOYING_STATE,
        status_reason: VCAP::CloudController::DeploymentModel::DEPLOYING_STATUS_REASON
      )
    end

    context 'with an admin who can see all deployments' do
      let(:admin_user_header) { headers_for(user, scopes: %w[cloud_controller.admin]) }

      let(:droplet2) { VCAP::CloudController::DropletModel.make(guid: 'droplet2') }
      let(:droplet3) { VCAP::CloudController::DropletModel.make(guid: 'droplet3') }
      let(:droplet4) { VCAP::CloudController::DropletModel.make(guid: 'droplet4') }
      let(:droplet5) { VCAP::CloudController::DropletModel.make(guid: 'droplet5') }
      let(:app2) { droplet2.app }
      let(:app3) { droplet3.app }
      let(:app4) { droplet4.app }
      let(:app5) { droplet5.app }

      before do
        app2.update(space:)
        app3.update(space:)
        app4.update(space:)
        app5.update(space:)
      end

      let!(:deployment2) do
        VCAP::CloudController::DeploymentModelTestFactory.make(app: app2, droplet: droplet2,
                                                               previous_droplet: droplet2,
                                                               status_value: VCAP::CloudController::DeploymentModel::ACTIVE_STATUS_VALUE,
                                                               state: VCAP::CloudController::DeploymentModel::CANCELING_STATE,
                                                               status_reason: VCAP::CloudController::DeploymentModel::CANCELING_STATUS_REASON)
      end

      let!(:deployment3) do
        VCAP::CloudController::DeploymentModelTestFactory.make(app: app3, droplet: droplet3,
                                                               previous_droplet: droplet3,
                                                               status_value: VCAP::CloudController::DeploymentModel::FINALIZED_STATUS_VALUE,
                                                               state: VCAP::CloudController::DeploymentModel::DEPLOYED_STATE,
                                                               status_reason: VCAP::CloudController::DeploymentModel::DEPLOYED_STATUS_REASON)
      end

      let!(:deployment4) do
        VCAP::CloudController::DeploymentModelTestFactory.make(app: app4, droplet: droplet4,
                                                               previous_droplet: droplet4,
                                                               status_value: VCAP::CloudController::DeploymentModel::FINALIZED_STATUS_VALUE,
                                                               state: VCAP::CloudController::DeploymentModel::CANCELED_STATE,
                                                               status_reason: VCAP::CloudController::DeploymentModel::CANCELED_STATUS_REASON)
      end

      let!(:deployment5) do
        VCAP::CloudController::DeploymentModelTestFactory.make(app: app5, droplet: droplet5,
                                                               previous_droplet: droplet5,
                                                               status_value: VCAP::CloudController::DeploymentModel::FINALIZED_STATUS_VALUE,
                                                               state: VCAP::CloudController::DeploymentModel::DEPLOYED_STATE,
                                                               status_reason: VCAP::CloudController::DeploymentModel::SUPERSEDED_STATUS_REASON)
      end

      def json_for_deployment(deployment, app_model, droplet, status_value, status_reason, cancel_link=true)
        {
          guid: deployment.guid,
          status: {
            value: status_value,
            reason: status_reason,
            details: {
              last_successful_healthcheck: iso8601
            }
          },
          strategy: 'rolling',
          droplet: {
            guid: droplet.guid
          },
          revision: nil,
          # previous_droplet: { guid: nil },
          previous_droplet: {
            guid: droplet.guid
          },
          new_processes: [{
            guid: deployment.deploying_web_process.guid,
            type: deployment.deploying_web_process.type
          }],
          created_at: iso8601,
          updated_at: iso8601,
          metadata: {
            labels: {},
            annotations: {}
          },
          relationships: {
            app: {
              data: {
                guid: app_model.guid
              }
            }
          },
          links: {
            self: {
              href: "#{link_prefix}/v3/deployments/#{deployment.guid}"
            },
            app: {
              href: "#{link_prefix}/v3/apps/#{app_model.guid}"
            }
          }
        }.tap do |json|
          if cancel_link
            json[:links][:cancel] = {
              href: "#{link_prefix}/v3/deployments/#{deployment.guid}/actions/cancel",
              method: 'POST'
            }
          end
        end
      end

      it 'lists all deployments' do
        get '/v3/deployments?per_page=2', nil, admin_user_header
        expect(last_response.status).to eq(200)

        parsed_response = Oj.load(last_response.body)
        expect(parsed_response).to match_json_response({
                                                         pagination: {
                                                           total_results: 5,
                                                           total_pages: 3,
                                                           first: {
                                                             href: "#{link_prefix}/v3/deployments?page=1&per_page=2"
                                                           },
                                                           last: {
                                                             href: "#{link_prefix}/v3/deployments?page=3&per_page=2"
                                                           },
                                                           next: {
                                                             href: "#{link_prefix}/v3/deployments?page=2&per_page=2"
                                                           },
                                                           previous: nil
                                                         },
                                                         resources: [
                                                           json_for_deployment(deployment, app_model, droplet,
                                                                               VCAP::CloudController::DeploymentModel::ACTIVE_STATUS_VALUE,
                                                                               VCAP::CloudController::DeploymentModel::DEPLOYING_STATUS_REASON),
                                                           json_for_deployment(deployment2, app2, droplet2,
                                                                               VCAP::CloudController::DeploymentModel::ACTIVE_STATUS_VALUE,
                                                                               VCAP::CloudController::DeploymentModel::CANCELING_STATUS_REASON)
                                                         ]
                                                       })
      end

      context 'when filtering' do
        let(:api_call) { ->(user_headers) { get endpoint, nil, user_headers } }

        describe 'when filtering by status_value' do
          let(:url) { '/v3/deployments' }
          let(:query) { 'status_values=FINALIZED' }
          let(:endpoint) { "#{url}?#{query}" }
          let(:expected_codes_and_responses) do
            h = Hash.new(
              code: 200,
              response_objects: [
                json_for_deployment(deployment3, app3, droplet3,
                                    VCAP::CloudController::DeploymentModel::FINALIZED_STATUS_VALUE,
                                    VCAP::CloudController::DeploymentModel::DEPLOYED_STATUS_REASON,
                                    false),
                json_for_deployment(deployment4, app4, droplet4,
                                    VCAP::CloudController::DeploymentModel::FINALIZED_STATUS_VALUE,
                                    VCAP::CloudController::DeploymentModel::CANCELED_STATUS_REASON,
                                    false),
                json_for_deployment(deployment5, app5, droplet5,
                                    VCAP::CloudController::DeploymentModel::FINALIZED_STATUS_VALUE,
                                    VCAP::CloudController::DeploymentModel::SUPERSEDED_STATUS_REASON,
                                    false)
              ]
            )
            h['org_billing_manager'] = h['org_auditor'] = h['no_role'] = {
              code: 200,
              response_objects: []
            }
            h
          end

          it_behaves_like 'permissions for list endpoint', ALL_PERMISSIONS

          context 'pagination' do
            let(:pagination_hsh) do
              {
                'total_results' => 3,
                'total_pages' => 1,
                'first' => { 'href' => "#{link_prefix}#{url}?page=1&per_page=50&#{query}" },
                'last' => { 'href' => "#{link_prefix}#{url}?page=1&per_page=50&#{query}" },
                'next' => nil,
                'previous' => nil
              }
            end

            it 'paginates the results' do
              get endpoint, nil, admin_header
              expect(pagination_hsh).to eq(parsed_response['pagination'])
            end
          end
        end

        describe 'when filtering by status_reason' do
          let(:url) { '/v3/deployments' }
          let(:query) { 'status_reasons=SUPERSEDED,DEPLOYED' }
          let(:endpoint) { "#{url}?#{query}" }
          let(:expected_codes_and_responses) do
            h = Hash.new(
              code: 200,
              response_objects: [
                json_for_deployment(deployment3, app3, droplet3,
                                    VCAP::CloudController::DeploymentModel::FINALIZED_STATUS_VALUE,
                                    VCAP::CloudController::DeploymentModel::DEPLOYED_STATUS_REASON,
                                    false),
                json_for_deployment(deployment5, app5, droplet5,
                                    VCAP::CloudController::DeploymentModel::FINALIZED_STATUS_VALUE,
                                    VCAP::CloudController::DeploymentModel::SUPERSEDED_STATUS_REASON,
                                    false)
              ]
            )
            h['org_billing_manager'] = h['org_auditor'] = h['no_role'] = {
              code: 200,
              response_objects: []
            }
            h
          end

          it_behaves_like 'permissions for list endpoint', ALL_PERMISSIONS

          context 'pagination' do
            let(:pagination_hsh) do
              {
                'total_results' => 2,
                'total_pages' => 1,
                'first' => { 'href' => "#{link_prefix}#{url}?page=1&per_page=50&#{query.gsub(',', '%2C')}" },
                'last' => { 'href' => "#{link_prefix}#{url}?page=1&per_page=50&#{query.gsub(',', '%2C')}" },
                'next' => nil,
                'previous' => nil
              }
            end

            it 'paginates the results' do
              get endpoint, nil, admin_header
              expect(pagination_hsh).to eq(parsed_response['pagination'])
            end
          end
        end

        describe 'when filtering by state' do
          let(:url) { '/v3/deployments' }
          let(:query) { 'states=DEPLOYING' }
          let(:endpoint) { "#{url}?#{query}" }
          let(:expected_codes_and_responses) do
            h = Hash.new(
              code: 200,
              response_objects: [
                json_for_deployment(deployment, app_model, droplet,
                                    VCAP::CloudController::DeploymentModel::ACTIVE_STATUS_VALUE,
                                    VCAP::CloudController::DeploymentModel::DEPLOYING_STATUS_REASON)
              ]
            )
            h['org_billing_manager'] = h['org_auditor'] = h['no_role'] = {
              code: 200,
              response_objects: []
            }
            h
          end

          it_behaves_like 'permissions for list endpoint', ALL_PERMISSIONS

          context 'pagination' do
            let(:pagination_hsh) do
              {
                total_results: 1,
                total_pages: 1,
                first: { href: "#{link_prefix}#{url}?page=1&per_page=50&#{query.gsub(',', '%2C')}" },
                last: { href: "#{link_prefix}#{url}?page=1&per_page=50&#{query.gsub(',', '%2C')}" },
                next: nil,
                previous: nil
              }
            end

            it 'paginates the results' do
              get endpoint, nil, admin_header

              expect(parsed_response['pagination']).to match_json_response(pagination_hsh)
            end
          end
        end

        it 'returns a list of label filtered deployments' do
          VCAP::CloudController::DeploymentLabelModel.make(
            key_name: 'release',
            value: 'stable',
            resource_guid: deployment2.guid
          )
          VCAP::CloudController::DeploymentLabelModel.make(
            key_name: 'release',
            value: 'unstable',
            resource_guid: deployment3.guid
          )

          get '/v3/deployments?label_selector=release=stable', nil, admin_user_header
          expect(last_response.status).to eq(200)

          expect(parsed_response['resources']).to have(1).items
          expect(parsed_response['resources'][0]['guid']).to eq(deployment2.guid)
        end
      end
    end

    context 'when there are other spaces the developer cannot see' do
      let(:user) { make_developer_for_space(space) }
      let(:another_app) { another_droplet.app }
      let(:another_droplet) { VCAP::CloudController::DropletModel.make }
      let!(:another_space) { another_app.space }
      let!(:another_deployment) { VCAP::CloudController::DeploymentModelTestFactory.make(app: another_app, droplet: another_droplet) }

      let(:user_header) { headers_for(user) }

      it_behaves_like 'list query endpoint' do
        let(:request) { 'v3/deployments' }
        let(:message) { VCAP::CloudController::DeploymentsListMessage }
        let(:params) do
          {
            page: '2',
            per_page: '10',
            order_by: 'updated_at',
            states: 'foo',
            status_values: 'foo',
            status_reasons: 'foo',
            app_guids: '123',
            label_selector: 'bar',
            guids: 'foo,bar',
            created_ats: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}",
            updated_ats: { gt: Time.now.utc.iso8601 }
          }
        end
      end

      it 'does not include the deployments in the other space' do
        get '/v3/deployments', nil, user_header
        expect(last_response.status).to eq(200)

        parsed_response = Oj.load(last_response.body)
        expect(parsed_response).to be_a_response_like({
                                                        'pagination' => {
                                                          'total_results' => 1,
                                                          'total_pages' => 1,
                                                          'first' => {
                                                            'href' => "#{link_prefix}/v3/deployments?page=1&per_page=50"
                                                          },
                                                          'last' => {
                                                            'href' => "#{link_prefix}/v3/deployments?page=1&per_page=50"
                                                          },
                                                          'next' => nil,
                                                          'previous' => nil
                                                        },
                                                        'resources' => [
                                                          {
                                                            'guid' => deployment.guid,
                                                            'status' => {
                                                              'value' => VCAP::CloudController::DeploymentModel::ACTIVE_STATUS_VALUE,
                                                              'reason' => VCAP::CloudController::DeploymentModel::DEPLOYING_STATUS_REASON,
                                                              'details' => {
                                                                'last_successful_healthcheck' => iso8601
                                                              }
                                                            },
                                                            'strategy' => 'rolling',
                                                            'droplet' => {
                                                              'guid' => droplet.guid
                                                            },
                                                            'revision' => nil,
                                                            'previous_droplet' => {
                                                              'guid' => droplet.guid
                                                            },
                                                            'new_processes' => [{
                                                              'guid' => deployment.deploying_web_process.guid,
                                                              'type' => deployment.deploying_web_process.type
                                                            }],
                                                            'created_at' => iso8601,
                                                            'updated_at' => iso8601,
                                                            'metadata' => metadata,
                                                            'relationships' => {
                                                              'app' => {
                                                                'data' => {
                                                                  'guid' => app_model.guid
                                                                }
                                                              }
                                                            },
                                                            'links' => {
                                                              'self' => {
                                                                'href' => "#{link_prefix}/v3/deployments/#{deployment.guid}"
                                                              },
                                                              'app' => {
                                                                'href' => "#{link_prefix}/v3/apps/#{app_model.guid}"
                                                              },
                                                              'cancel' => {
                                                                'href' => "#{link_prefix}/v3/deployments/#{deployment.guid}/actions/cancel",
                                                                'method' => 'POST'
                                                              }
                                                            }
                                                          }
                                                        ]
                                                      })
      end
    end

    it_behaves_like 'list_endpoint_with_common_filters' do
      let(:resource_klass) { VCAP::CloudController::DeploymentModel }
      let(:api_call) do
        ->(headers, filters) { get "/v3/deployments?#{filters}", nil, headers }
      end
      let(:headers) { admin_headers }
    end
  end

  describe 'POST /v3/deployments/:guid/actions/cancel' do
    let(:old_droplet) { VCAP::CloudController::DropletModel.make(app: app_model, process_types: { 'web' => 'run' }) }
    let(:deployment) do
      VCAP::CloudController::DeploymentModelTestFactory.make(
        app: app_model,
        droplet: droplet,
        previous_droplet: old_droplet
      )
    end

    context 'with a running deployment' do
      let(:api_call) { ->(user_headers) { post "/v3/deployments/#{deployment.guid}/actions/cancel", {}.to_json, user_headers } }
      let(:expected_codes_and_responses) do
        h = Hash.new(code: 404)
        h['admin'] = h['space_developer'] = h['space_supporter'] = { code: 200 }
        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: 404 } }
          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 deployment is running and has a previous droplet' do
      let(:user) { make_developer_for_space(space) }

      it 'changes the deployment status_value CANCELING and rolls the droplet back' do
        post "/v3/deployments/#{deployment.guid}/actions/cancel", {}.to_json, user_header
        expect(last_response.status).to eq(200), last_response.body

        expect(last_response.body).to be_empty
        deployment.reload
        expect(deployment.status_value).to eq(VCAP::CloudController::DeploymentModel::ACTIVE_STATUS_VALUE)
        expect(deployment.status_reason).to eq(VCAP::CloudController::DeploymentModel::CANCELING_STATUS_REASON)

        expect(app_model.reload.droplet).to eq(old_droplet)

        require 'cloud_controller/deployment_updater/scheduler'
        VCAP::CloudController::DeploymentUpdater::Updater.new(deployment, Steno.logger('blah')).cancel
        deployment.reload
        expect(deployment.status_value).to eq(VCAP::CloudController::DeploymentModel::FINALIZED_STATUS_VALUE)
        expect(deployment.status_reason).to eq(VCAP::CloudController::DeploymentModel::CANCELED_STATUS_REASON)
      end
    end
  end
end