# coding: UTF-8

require 'spec_helper'
require 'em-http'
require 'dea/config'

require 'dea/directory_server/directory_server_v2'
require 'dea/staging/staging_task'

describe Dea::StagingTask do
  let(:loggregator_emitter) { FakeEmitter.new }

  let(:memory_limit_mb) { 256 }
  let(:disk_limit_mb) { 1025 }
  let(:disk_inode_limit) { 12345 }

  let!(:workspace_dir) do
    staging_task.workspace.workspace_dir # force workspace creation
  end

  let(:max_staging_duration) { 900 }
  let(:rootfs) { '/var/path/to/rootfs' }

  let(:config) do
    {
      'base_dir' => base_dir,
      'directory_server' => {
        'file_api_port' => 1234
      },
      'stacks' => [
        {
          'name' => attributes['stack'],
          'package_path' => rootfs,
        },
        {
          'name' => 'not-my-stack',
          'package_path' => 'where_is_this',
        }
      ],
      'staging' => {
        'cpu_limit_shares' => 512,
        'memory_limit_mb' => memory_limit_mb,
        'disk_limit_mb' => disk_limit_mb,
        'disk_inode_limit' => disk_inode_limit,
        'max_staging_duration' => max_staging_duration,
        'bandwidth_limit' => { 'rate' => 20_000_000, 'burst' => 100_000_000 },
      },
    }
  end

  let(:base_dir) { Dir.mktmpdir('base_dir') }
  let(:bootstrap) { double(:bootstrap, config: Dea::Config.new(config), snapshot: double(:snapshot)) }
  let(:dir_server) { Dea::DirectoryServerV2.new('domain', 1234, nil, config) }

  let(:logger) do
    double('logger').tap do |l|
      %w(debug debug2 info warn log_exception error).each { |m| allow(l).to receive(m) }
    end
  end

  let(:attributes) { valid_staging_attributes }

  let(:buildpacks_in_use) do
    [ {key: 'buildpack1', uri: URI('http://www.goolge.com')},
      {key: 'buildpack2', uri: URI('http://www.goolge2.com')}
    ]
  end

  let(:successful_promise) { Dea::Promise.new { |p| p.deliver } }

  let(:failing_promise) { Dea::Promise.new { |p| raise 'failing promise' } }

  let (:empty_streams) { double(:stdout => '', :stderr => '') }

  let(:staging_message) { StagingMessage.new(attributes) }

  subject(:staging_task) { Dea::StagingTask.new(bootstrap, dir_server, staging_message, buildpacks_in_use) }

  after { FileUtils.rm_rf(workspace_dir) if File.exists?(workspace_dir) }

  before do
    allow(staging_task).to receive(:workspace_dir) { workspace_dir }
    allow(staging_task).to receive(:staged_droplet_path) { __FILE__ }
    allow(staging_task).to receive(:downloaded_app_package_path) { '/path/to/downloaded/droplet' }
    allow(staging_task).to receive(:logger) { logger }
    allow(staging_task).to receive(:container_exists?) { true }

    Dea::Loggregator.staging_emitter = loggregator_emitter
  end

  describe '#promise_stage' do
    let(:spawn_response) { double(job_id: 25) }

    before do
      allow(staging_task.container).to receive(:spawn) { spawn_response }
      allow(staging_task.container).to receive(:link_or_raise)
      allow(staging_task.bootstrap.snapshot).to receive(:save)
    end

    describe 'assembles staging command correctly' do
      it 'calls the container#spawn with the staging command' do
        expect(staging_task.container).to receive(:spawn) do |cmd|
          expect(cmd).to include 'export FOO="BAR";'
          expect(cmd).to include 'export STAGING_TIMEOUT="900.0";'
          expect(cmd).to include 'export MEMORY_LIMIT="512m";' # the user assiged 512 should overwrite the system 256
          expect(cmd).to include 'export VCAP_SERVICES=\{\}'

          expect(cmd).to match %r{.*/bin/run .*/plugin_config | tee -a}

          spawn_response
        end

        with_event_machine do
          staging_task.promise_stage.resolve
          done
        end
      end

      context 'when env variables need to be escaped' do
        before { attributes['properties']['environment'] = ['PATH=x y z', "FOO=z'y\"d", 'BAR=', 'BAZ=foo=baz'] }

        it 'copes with spaces' do
          allow(staging_task.container).to receive(:spawn) do |cmd|
            expect(cmd).to include('export PATH="x y z";')

            spawn_response
          end

          with_event_machine do
            staging_task.promise_stage.resolve
            done
          end
        end

        it 'copes with quotes' do
          allow(staging_task.container).to receive(:spawn) do |cmd|
            expect(cmd).to include(%Q{export FOO="z'y\\"d";})
          end.and_return(spawn_response)

          with_event_machine do
            staging_task.promise_stage.resolve
            done
          end
        end

        it 'copes with blank' do
          allow(staging_task.container).to receive(:spawn) do |cmd|
            expect(cmd).to include('export BAR="";')

            spawn_response
          end

          with_event_machine do
            staging_task.promise_stage.resolve
            done
          end
        end

        it 'copes with equal sign' do
          allow(staging_task.container).to receive(:spawn) do |cmd|
            expect(cmd).to include('export BAZ="foo=baz";')
          end.and_return(spawn_response)

          with_event_machine do
            staging_task.promise_stage.resolve
            done
          end
        end
      end
    end

    it 'saves warden job id to the snapshot' do
      expect(staging_task.snapshot_attributes['warden_job_id']).to be_nil

      expect(staging_task.bootstrap.snapshot).to receive(:save)

      with_event_machine do
        staging_task.promise_stage.resolve
        done
      end

      expect(staging_task.snapshot_attributes['warden_job_id']).to eq(25)
    end

    it 'links to the job' do
      expect(staging_task.container).to receive(:link_or_raise).with(25)

      with_event_machine do
        staging_task.promise_stage.resolve
        done
      end
    end

    context 'when job fails' do
      let (:staging_result) { double(:stdout => 'stdout message', :stderr => 'stderr message') }
      let (:staging_error) { Container::WardenError.new('Failed to stage', staging_result) }

      before { allow(staging_task.container).to receive(:link_or_raise).and_raise(staging_error) }

      it 'raises Container::WardenError' do

        with_event_machine do
          expect { staging_task.promise_stage.resolve }.to raise_error(Container::WardenError)
          done
        end
      end
    end

    context 'when job exceeds staging timeout' do
      let(:max_staging_duration) { 0.1 }
      let(:container_staging_duration) { 0.2 }

      it 'fails with a TimeoutError' do
        stop_request = ::Warden::Protocol::StopRequest.new(handle: staging_task.container.handle, kill: true)
        allow(staging_task.container).to receive(:call).with(:stop, stop_request)

        allow(staging_task.container).to receive(:link_or_raise) do
          f = Fiber.current

          EM.add_timer(container_staging_duration) do
            f.resume
          end

          Fiber.yield
        end

        with_event_machine do
          Fiber.new do
            expect { staging_task.promise_stage.resolve }.to raise_error('Staging in container timed out')
            done
          end.resume
        end
      end
    end
  end

  describe '#task_log' do
    describe 'when staging has not yet started' do
      subject { staging_task.task_log }
      it 'is nil' do
        expect(subject).to be_nil
      end
    end

    describe 'once staging has started' do
      before do
        File.open(File.join(workspace_dir, 'staging_task.log'), 'w') do |f|
          f.write 'some log content'
        end
      end

      it 'reads the staging_task log file' do
        expect(staging_task.task_log).to eq('some log content')
      end
    end
  end

  describe '#task_info' do
    context 'when staging info file exists' do
      before do
        contents = <<YAML
---
detected_buildpack: Ruby/Rack
YAML
        staging_info = File.join(workspace_dir, 'staging_info.yml')
        File.open(staging_info, 'w') { |f| f.write(contents) }
      end

      it 'parses staging info file' do
        expect(staging_task.task_info['detected_buildpack']).to eq('Ruby/Rack')
      end
    end

    context 'when staging info file does not exist' do
      it 'returns empty hash if' do
        expect(staging_task.task_info).to be_empty
      end
    end
  end

  describe 'procfile' do
    before do
      contents = <<YAML
---
effective_procfile:
  web: npm start
YAML
      staging_info = File.join(workspace_dir, 'staging_info.yml')
      File.open(staging_info, 'w') { |f| f.write(contents) }
    end

    it 'returns the detected buildpack' do
      expect(staging_task.procfile).to eq({"web" => "npm start"})
    end
  end

  describe '#detected_buildpack' do
    before do
      contents = <<YAML
---
detected_buildpack: Ruby/Rack
YAML
      staging_info = File.join(workspace_dir, 'staging_info.yml')
      File.open(staging_info, 'w') { |f| f.write(contents) }
    end

    it 'returns the detected buildpack' do
      expect(staging_task.detected_buildpack).to eq('Ruby/Rack')
    end
  end

  describe '#detected_start_command' do
    before do
      contents = <<YAML
---
start_command: bacofoil
YAML
      staging_info = File.join(workspace_dir, 'staging_info.yml')
      File.open(staging_info, 'w') { |f| f.write(contents) }
    end

    it 'returns the detected start command' do
      expect(staging_task.detected_start_command).to eq('bacofoil')
    end
  end

  describe '#buildpack_path' do
    before do
      contents = <<YAML
---
buildpack_path: some/buildpack/path
YAML
      staging_info = File.join(workspace_dir, 'staging_info.yml')
      File.open(staging_info, 'w') { |f| f.write(contents) }
    end

    it 'returns the buildpack path' do
      expect(staging_task.buildpack_path).to eq('some/buildpack/path')
    end
  end

  describe '#buildpack_key' do
    let(:buildpack_path) { "#{staging_task.workspace.admin_buildpacks_dir}/admin_key" }

    before do
      FileUtils.mkdir_p(staging_task.workspace.admin_buildpacks_dir)
      FileUtils.mkdir_p(buildpack_path)
      contents = <<YAML
---
buildpack_path: #{buildpack_path}
YAML
      staging_info = File.join(workspace_dir, 'staging_info.yml')
      File.open(staging_info, 'w') { |f| f.write(contents) }
    end

    context 'when an admin buildpack is detected' do
      it 'returns the correct buildpack key' do
        expect(staging_task.buildpack_key).to eq('admin_key')
      end
    end

    context 'when an admin buildpack is specified' do
      let(:buildpack_path) { "#{staging_task.workspace.admin_buildpacks_dir}/ignored" }
      let(:attributes) do
        staging_attributes = valid_staging_attributes
        staging_attributes['properties']['buildpack_key'] = 'specified_admin_key'
        staging_attributes
      end

      it 'returns the specified admin key' do
        expect(staging_task.buildpack_key).to eq('specified_admin_key')
      end
    end

    context 'when a detected system buildpack is used' do
      let(:buildpack_path) { "#{staging_task.workspace.system_buildpacks_dir}/java" }

      it 'returns a nil buildpack key' do
        expect(staging_task.buildpack_key).to be_nil
      end
    end
  end

  describe '#error_info' do
    let(:error_type) { "NoAppDetectedError" }
    let(:error_message) { "An application could not be detected..." }

    context 'when a staging error is present' do
      before do
        contents = <<YAML
---
staging_error:
  type: #{error_type}
  message: #{error_message}
YAML
        staging_info = File.join(workspace_dir, 'staging_info.yml')
        File.open(staging_info, 'w') { |f| f.write(contents) }
      end

      it 'returns a hash with the error type and message' do
        expect(staging_task.error_info['type']).to eq(error_type)
        expect(staging_task.error_info['message']).to eq(error_message)
      end
    end

    context 'when a staging error is not present' do
      it 'returns returns nil' do
        expect(staging_task.error_info).to be_nil
      end
    end
  end

  describe '#streaming_log_url' do
    let(:url) { staging_task.streaming_log_url }

    it 'returns url for staging log' do
      expect(url).to include("/staging_tasks/#{staging_task.task_id}/file_path",)
    end

    it 'includes path to staging task output' do
      expect(url).to include 'path=%2Ftmp%2Fstaged%2Flogs%2Fstaging_task.log'
    end

    it 'hmacs url' do
      expect(url).to match(/hmac=.*/)
    end
  end

  describe '#path_in_container' do
    context 'when given path is not nil' do
      context 'when container path is set' do
        before do
          allow(staging_task.container).to receive(:path).and_return('/container/path')
        end

        it 'returns path inside warden container root file system' do
          expect(staging_task.path_in_container('path/to/file')).to eq('/container/path/tmp/rootfs/path/to/file')
        end
      end

      context 'when container path is not set' do
        before { allow(staging_task.container).to receive(:path).and_return(nil) }

        it 'returns nil' do
          expect(staging_task.path_in_container('path/to/file')).to be_nil
        end
      end
    end

    context 'when given path is nil' do
      context 'when container path is set' do
        before do
          allow(staging_task.container).to receive(:path).and_return('/container/path')
        end

        it 'returns path inside warden container root file system' do
          expect(staging_task.path_in_container(nil)).to eq('/container/path/tmp/rootfs/')
        end
      end

      context 'when container path is not set' do
        before { allow(staging_task).to receive(:container_path).and_return(nil) }

        it 'returns nil' do
          expect(staging_task.path_in_container('path/to/file')).to be_nil
        end
      end
    end
  end

  describe '#start' do
    def stub_staging_setup
      %w(
         app_download
         buildpack_cache_download
         prepare_staging_log
         app_dir
      ).each do |step|
        allow(staging_task).to receive("promise_#{step}").and_return(successful_promise)
      end
      allow(staging_task.container).to receive(:create_container)
      allow(staging_task.container).to receive(:update_path_and_ip)
    end

    def stub_staging
      %w(unpack_app
         unpack_buildpack_cache
         stage
         pack_app
         copy_out
         save_droplet
         log_upload_started
         app_upload
         pack_buildpack_cache
         copy_out_buildpack_cache
         buildpack_cache_upload
         staging_info
         task_log
         destroy
      ).each do |step|
        allow(staging_task).to receive("promise_#{step}").and_return(successful_promise)
      end
    end

    def stub_staging_upload
      %w(
      app_upload
      save_buildpack_cache
      destroy
      ).each do |step|
        allow(staging_task).to receive("promise_#{step}").and_return(successful_promise)
      end
    end

    def self.it_calls_callback(callback_name, options={})
      describe "after_#{callback_name}_callback" do
        before do
          stub_staging_setup
          stub_staging
          stub_staging_upload
        end

        context 'when there is no callback registered' do
          it "does not try to call any callbacks" do
            staging_task.start
          end
        end

        context 'when there is callback registered' do
          before do
            @received_count = 0
            @received_error = nil
            staging_task.send("after_#{callback_name}_callback") do |error|
              @received_count += 1
              @received_error = error
            end
          end

          context "and staging task succeeds finishing #{callback_name}" do
            it 'calls registered callback without an error' do
              staging_task.start
              expect(@received_count).to eq(1)
              expect(@received_error).to be_nil
            end
          end

          context "and staging task fails before finishing #{callback_name}" do
            before { allow(staging_task).to receive(options[:failure_cause]).and_return(failing_promise) }

            it 'calls registered callback with an error' do
              staging_task.start rescue nil
              expect(@received_count).to eq(1)
              expect(@received_error.to_s).to eq('failing promise')
            end
          end

          context 'and the callback itself fails' do
            before do
              staging_task.send("after_#{callback_name}_callback") do |_|
                @received_count += 1
                raise 'failing callback'
              end
            end

            it 'cleans up workspace' do
              expect {
                staging_task.start
              }.to change { File.exists?(workspace_dir) }.from(true).to(false)
            end if options[:callback_failure_cleanup_assertions]

            it 'calls registered callback exactly once' do
              staging_task.start
              expect(@received_count).to eq(1)
            end

            context 'and there is no error from staging' do
              it 'does not raises an error from the callback' do
                staging_task.start
              end
            end

            context 'and there is an error from staging' do
              before { allow(staging_task).to receive(options[:failure_cause]).and_return(failing_promise) }

              it 'does not raise the staging error' do
                staging_task.start
              end
            end
          end
        end
      end
    end

    it_calls_callback :setup, :failure_cause => :promise_app_download

    it_calls_callback :complete, {
      :failure_cause => :promise_stage,
      :callback_failure_cleanup_assertions => true
    }

    context 'when finished' do
      before do
        stub_staging_setup
        allow(staging_task).to receive(:resolve_staging)
        stub_staging_upload
      end

      it 'should close all connections' do
        expect(staging_task.container).to receive(:close_all_connections)
        staging_task.start
      end
    end

    it 'should clean up after itself' do
      allow(staging_task.workspace).to receive(:prepare).and_raise('Error')
      stub_staging_upload

      staging_task.start
      expect(File.exists?(workspace_dir)).to be false
    end

    context 'when a script fails' do
      before do
        stub_staging_setup
        stub_staging
        allow(staging_task).to receive(:promise_stage).and_raise('Script Failed')
      end

      it 'still copies out the task log' do
        allow(staging_task).to receive(:promise_task_log) { double('promise', :resolve => nil) }
        staging_task.start rescue nil
      end

      it 'returns an error in response' do
        response = nil
        staging_task.after_complete_callback do |callback_response|
          response = callback_response
        end

        staging_task.start

        expect(response.message).to match /Script Failed/
      end

      it 'does not uploads droplet' do
        expect(staging_task).to_not receive(:resolve_staging_upload)
        staging_task.start rescue nil
      end
    end

    describe '#bind_mounts' do
      it 'includes the workspace dir' do
        expect(staging_task.bind_mounts).to include('src_path' => staging_task.workspace.workspace_dir,
                                              'dst_path' => staging_task.workspace.workspace_dir)
      end

      it 'includes the build pack url' do
        expect(staging_task.bind_mounts).to include('src_path' => staging_task.workspace.buildpack_dir,
                                           'dst_path' => staging_task.workspace.buildpack_dir)
      end

      it 'includes the configured bind mounts' do
        mount = {
          'src_path' => 'a',
          'dst_path' => 'b'
        }
        staging_task.config['bind_mounts'] = [mount]
        expect(staging_task.bind_mounts).to include(mount)
      end
    end

    it 'performs staging setup operations in correct order' do
      with_network = false
      expected_bandwidth = { rate: 20_000_000, burst: 100_000_000 }
      allow(staging_task.workspace).to receive(:prepare).ordered
      staging_task.workspace.workspace_dir
      allow(staging_task.container).to receive(:create_container).
        with(bind_mounts: staging_task.bind_mounts,
             limit_cpu: staging_task.staging_config['cpu_limit_shares'],
             byte: staging_task.disk_limit_in_bytes,
             inode: staging_task.disk_inode_limit,
             limit_memory: staging_task.memory_limit_in_bytes,
             setup_inbound_network: with_network,
             egress_rules: staging_task.staging_message.egress_rules,
             rootfs: rootfs,
             limit_bandwidth: expected_bandwidth).ordered
      %w(
        promise_app_download
        promise_prepare_staging_log
        promise_app_dir
      ).each do |step|
        allow(staging_task).to receive(step).ordered.and_return(successful_promise)
      end
      allow(staging_task.container).to receive(:update_path_and_ip).ordered

      stub_staging
      stub_staging_upload
      staging_task.start
    end

    context 'when buildpack_cache_download_uri is provided' do
      subject(:staging_task) do
        Dea::StagingTask.new(
          bootstrap,
          dir_server,
          StagingMessage.new(attributes.merge('buildpack_cache_download_uri' => 'http://www.someurl.com')),
          buildpacks_in_use
        )
      end

      it 'downloads buildpack cache' do
        allow(staging_task).to receive(:promise_buildpack_cache_download)

        stub_staging
        stub_staging_setup

        staging_task.start
      end
    end

    it 'performs staging operations in correct order' do
      %w(unpack_app
         unpack_buildpack_cache
         stage
         pack_app
         copy_out
         save_droplet
         log_upload_started
         staging_info
         task_log
         ).each do |step|
        allow(staging_task).to receive("promise_#{step}").ordered.and_return(successful_promise)
      end

      stub_staging_setup
      stub_staging_upload
      staging_task.start
    end

    it 'performs staging upload operations in correct order' do
      %w(
      app_upload
      save_buildpack_cache
      destroy
      ).each do |step|
        allow(staging_task).to receive("promise_#{step}").ordered.and_return(successful_promise)
      end

      stub_staging_setup
      stub_staging
      staging_task.start
    end

    it 'triggers callbacks in correct order' do
      stub_staging_setup
      stub_staging
      stub_staging_upload

      allow(staging_task).to receive(:resolve_staging).ordered
      allow(staging_task).to receive(:resolve_staging_upload).ordered.and_call_original
      allow(staging_task).to receive(:promise_app_upload).ordered
      allow(staging_task).to receive(:promise_save_buildpack_cache).ordered
      allow(staging_task).to receive(:trigger_after_complete).ordered

      staging_task.start
    end

    context 'when the rootfs does not exist' do
      before do
        config['stacks'][0]['name'] = 'wrong-name'
      end

      it 'reports an error' do
        response = nil
        staging_task.after_complete_callback do |callback_response|
          response = callback_response
        end

        stub_staging
        stub_staging_upload
        staging_task.start
        expect(response.message).to match(/Stack my-stack does not exist/)
      end
    end

    context 'when the upload fails' do
      let(:some_terrible_error) { RuntimeError.new('error') }
      before do
        stub_staging_setup
        stub_staging
        stub_staging_upload
      end

      def it_raises_an_error
        response = nil
        staging_task.after_complete_callback do |callback_response|
          response = callback_response
        end

        staging_task.start
        expect(response).to eq(some_terrible_error)
      end

      it 'copes with uploading errors' do
        allow(staging_task).to receive(:promise_app_upload).and_raise(some_terrible_error)

        it_raises_an_error
      end

      it 'copes with buildpack cache errors' do
        allow(staging_task).to receive(:promise_save_buildpack_cache).and_raise(some_terrible_error)

        it_raises_an_error
      end
    end
  end

  describe '#stop' do
    context 'if container exists' do
      before { allow(staging_task.container).to receive(:handle) { 'maria' } }
      it 'sends stop request to warden container' do
        allow(staging_task).to receive(:promise_stop).and_return(successful_promise)
        staging_task.stop
      end
    end

    context 'if container does not exist' do
      before { allow(staging_task.container).to receive(:handle) { nil } }
      it 'does NOT send stop request to warden container' do
        expect(staging_task).to_not receive(:promise_stop)
        staging_task.stop
      end
    end

    it 'calls the callback' do
      callback = lambda {}
      allow(callback).to receive(:call)
      staging_task.stop(&callback)
    end

    it 'triggers after stop callback' do
      allow(staging_task).to receive(:trigger_after_stop)
      staging_task.stop
    end

    it 'unregisters after complete callback' do
      allow(staging_task).to receive(:resolve_staging_setup)
      allow(staging_task).to receive(:resolve_staging_upload)
      allow(staging_task).to receive(:promise_destroy).and_return(successful_promise)
      # Emulate staging stop while running staging_task
      allow(staging_task).to receive(:resolve_staging) { staging_task.stop }

      expect(staging_task).to_not receive(:after_complete_callback)
      staging_task.start
    end
  end

  describe '#memory_limit_in_bytes' do
    context 'when unspecified' do
      before do
        config['staging'].delete('memory_limit_mb')
      end

      it 'uses 1GB as a default' do
        expect(staging_task.memory_limit_in_bytes).to eq(1024*1024*1024)
      end
    end

    context 'when the app requests less than the config' do
      before do
        config['staging']['memory_limit_mb'] = 1024
        attributes['start_message']['limits']['mem'] = 512
      end

      it 'sets the memory_limit_in_bytes to the config value' do
        expect(staging_task.memory_limit_in_bytes).to eq(1024*1024*1024)
      end
    end

    context 'when the app requests more than the config' do
      before do
        config['staging']['memory_limit_mb'] = 1024
        attributes['start_message']['limits']['mem'] = 2048
      end

      it 'sets the memory_limit_in_bytes to the app value' do
        expect(staging_task.memory_limit_in_bytes).to eq(2048*1024*1024)
      end
    end
  end

  describe '#disk_limit_in_bytes' do
    it 'exports disk in bytes as specified in the config file' do
      expect(staging_task.disk_limit_in_bytes).to eq(1024 * 1024 * disk_limit_mb)
    end

    context 'when unspecified' do
      before do
        config['staging'].delete('disk_limit_mb')
      end

      it 'uses 2GB as a default' do
        expect(staging_task.disk_limit_in_bytes).to eq(2*1024*1024*1024)
      end
    end
  end

  describe '#disk_limit_mb and #mem_limit_mb' do
    context 'when specified in the staging message' do
      let(:mem_limit) { 1024 }
      let(:disk_limit) { 2048 }

      let(:attributes) do
        valid_staging_attributes.merge({
          "memory_limit" => mem_limit,
          "disk_limit" => disk_limit
        })
      end

      it 'returns the staging messages limit values' do
        expect(staging_task.disk_limit_mb).to eq(disk_limit)
        expect(staging_task.memory_limit_mb).to eq(mem_limit)
      end
    end

    context 'when unspecified' do
      let(:disk_limit_mb) { 3333 } # default staging disk limit of config object
      let(:memory_limit_mb) { 1234 }

      it 'returns the defaults' do
        expect(staging_task.disk_limit_mb).to eq(disk_limit_mb)
        expect(staging_task.memory_limit_mb).to eq(memory_limit_mb)
      end
    end
  end

  describe '#disk_inode_limit' do
    it 'exports disk with set inode as specified in the config file' do
      expect(staging_task.disk_inode_limit).to eq(disk_inode_limit)
    end
  end

  describe '#promise_prepare_staging_log' do
    it 'assembles a shell command that creates staging_task.log file for tailing it' do
      allow(staging_task.container).to receive(:run_script) do |connection_name, cmd|
        expect(cmd).to match 'mkdir -p /tmp/staged/logs && touch /tmp/staged/logs/staging_task.log'
      end
      staging_task.promise_prepare_staging_log.resolve
    end
  end

  describe '#promise_app_download' do
    subject do
      promise = staging_task.promise_app_download
      promise.resolve
      promise
    end

    let(:staging_app_file_path) { "#{workspace_dir}/app.zip" }

    context 'when there is an error' do
      before do
        allow_any_instance_of(Download).to receive(:download!).and_yield(
          RuntimeError.new('This is an error'))
      end

      it { expect { subject }.to raise_error(RuntimeError, 'This is an error') }

      it 'should not create an app file' do
        subject rescue nil
        expect(File.exists?(staging_task.workspace.downloaded_app_package_path)).to be false
      end
    end

    context 'when there is no error' do
      before do
        allow_any_instance_of(Download).to receive(:download!).and_yield(nil)
      end


      it "should deliver a nil" do
        expect(subject.result).to eq([:deliver, nil])
      end

      it 'should rename the file' do
        subject
        expect(File.exists?(staging_app_file_path)).to be true
        expect(sprintf('%o', File.stat(staging_app_file_path).mode)).to eq '100744'
      end
    end
  end

  describe '#promise_buildpack_cache_download' do
    subject do
      staging_task.workspace.prepare(staging_task.buildpack_manager)
      promise = staging_task.promise_buildpack_cache_download
      promise.resolve
      promise
    end

    let(:buildpack_cache_dest) { File.join workspace_dir, 'buildpack_cache.tgz' }

    context 'when there is an error' do
      before do
        allow_any_instance_of(Download).to receive(:download!).and_yield(
          RuntimeError.new('This is an error'))
      end

      it "should not raise an error" do
        expect(subject.result).to eq([:deliver, nil])
      end

      it 'does not create the buildpack cache tarball' do
        subject
        expect(File.exists?(buildpack_cache_dest)).to be false
      end
    end

    context 'when there is no error' do
      before { allow_any_instance_of(Download).to receive(:download!).and_yield(nil) }

      it "should return nil" do
        expect(subject.result).to eq([:deliver, nil])
      end

      it 'should rename the file' do
        subject
        expect(File.exists?(buildpack_cache_dest)).to be true
        expect(sprintf('%o', File.stat(buildpack_cache_dest).mode)).to eq '100744'
      end
    end
  end

  describe '#promise_unpack_app' do
    it 'assembles a shell command' do
      allow(staging_task.container).to receive(:run_script) do |connection_name, cmd|
        expect(cmd).to include("unzip -q #{workspace_dir}/app.zip -d /tmp/unstaged")

        empty_streams
      end
      staging_task.promise_unpack_app.resolve
    end

    it 'logs to loggregator' do
      allow(staging_task.container).to receive(:run_script).and_return(double(:stdout => 'stdout message', :stderr => 'stderr message'))
      staging_task.promise_unpack_app.resolve
      app_id = staging_task.staging_message.app_id
      expect(loggregator_emitter.messages.size).to eql(1)
      expect(loggregator_emitter.error_messages.size).to eql(1)
      expect(loggregator_emitter.messages[app_id][0]).to eql('stdout message')
      expect(loggregator_emitter.error_messages[app_id][0]).to eql('stderr message')
    end
  end

  describe '#promise_unpack_buildpack_cache' do
    context 'when buildpack cache does not exist' do
      it 'does not run a warden command' do
        expect(staging_task.container).to_not receive(:run_script)
        staging_task.promise_unpack_buildpack_cache.resolve
      end
    end

    context 'when buildpack cache exists' do
      before do
        FileUtils.touch("#{workspace_dir}/buildpack_cache.tgz")
      end

      it 'assembles a shell command' do
        allow(staging_task.container).to receive(:run_script) do |_, cmd|
          expect(cmd).to include("tar xfz #{workspace_dir}/buildpack_cache.tgz -C /tmp/cache")

          empty_streams
        end
        staging_task.promise_unpack_buildpack_cache.resolve
      end

      it 'logs to loggregator' do
        allow(staging_task.container).to receive(:run_script).and_return(double(:stdout => 'stdout message', :stderr => 'stderr message'))
        staging_task.promise_unpack_buildpack_cache.resolve
        app_id = staging_task.staging_message.app_id
        expect(loggregator_emitter.messages.size).to eql(1)
        expect(loggregator_emitter.error_messages.size).to eql(1)
        expect(loggregator_emitter.messages[app_id][0]).to eql('stdout message')
        expect(loggregator_emitter.error_messages[app_id][0]).to eql('stderr message')
      end
    end
  end

  describe '#promise_pack_app' do
    it 'assembles a shell command' do
      allow(staging_task.container).to receive(:run_script) do |connection_name, cmd|
        expect(normalize_whitespace(cmd)).to include('cd /tmp/staged && COPYFILE_DISABLE=true tar -czf /tmp/droplet.tgz .')
      end

      staging_task.promise_pack_app.resolve
    end
  end

  describe '#promise_pack_buildpack_cache' do
    it 'assembles a shell command' do
      allow(staging_task.container).to receive(:run_script) do |_, cmd|
        expect(normalize_whitespace(cmd)).to include('cd /tmp/cache && COPYFILE_DISABLE=true tar -czf /tmp/buildpack_cache.tgz .')
      end

      staging_task.promise_pack_buildpack_cache.resolve
    end
  end

  describe '#promise_save_buildpack_cache' do

    context 'when packing succeeds' do

      before do
        allow(staging_task).to receive(:promise_pack_buildpack_cache).and_return(successful_promise)
        allow(staging_task).to receive(:promise_copy_out_buildpack_cache).and_return(successful_promise)
        allow(staging_task).to receive(:promise_buildpack_cache_upload).and_return(successful_promise)
      end

      it 'copies out the buildpack cache' do
        allow(staging_task).to receive(:promise_copy_out_buildpack_cache).and_return(successful_promise)
        staging_task.promise_save_buildpack_cache.resolve
      end

      it 'uploads the buildpack cache' do
        allow(staging_task).to receive(:promise_buildpack_cache_upload).and_return(successful_promise)
        staging_task.promise_save_buildpack_cache.resolve
      end
    end

    context 'when packing fails' do

      before { allow(staging_task).to receive(:promise_pack_buildpack_cache).and_return(failing_promise) }

      it 'does not copy out the buildpack cache' do
        expect(staging_task).to_not receive :promise_copy_out_buildpack_cache
        staging_task.promise_save_buildpack_cache.resolve rescue nil
      end

      it 'does not upload the buildpack cache' do
        expect(staging_task).to_not receive :promise_buildpack_cache_upload
        staging_task.promise_save_buildpack_cache.resolve rescue nil
      end

    end
  end

  describe '#promise_app_upload' do
    subject do
      promise = staging_task.promise_app_upload
      promise.resolve
      promise
    end

    context 'when there is an error' do
      before do
        allow_any_instance_of(Upload).to receive(:upload!).and_yield(
          RuntimeError.new('This is an error'))
      end

      it { expect { subject }.to raise_error(RuntimeError, 'This is an error') }
    end

    context 'when there is no error' do
      before { allow_any_instance_of(Upload).to receive(:upload!).and_yield(nil) }

      it "should return nil" do
        expect(subject.result).to eq([:deliver, nil])
      end
    end
  end

  describe '#promise_buildpack_cache_upload' do
    subject do
      promise = staging_task.promise_buildpack_cache_upload
      promise.resolve
      promise
    end

    context 'when there is an error' do
      before do
        allow_any_instance_of(Upload).to receive(:upload!).and_yield(
          RuntimeError.new('This is an error'))
      end

      it { expect { subject }.to raise_error(RuntimeError, 'This is an error') }
    end

    context 'when there is no error' do
      before { allow_any_instance_of(Upload).to receive(:upload!).and_yield(nil) }

      it "should return nil" do
        expect(subject.result).to eq([:deliver, nil])
      end
    end
  end

  describe '#promise_copy_out' do
    subject do
      promise = staging_task.promise_copy_out
      promise.resolve
      promise
    end

    it 'should send copying out request' do
      allow(staging_task).to receive(:copy_out_request).with('/tmp/droplet.tgz', /.{5,}/)
      subject
    end
  end

  describe '#promise_save_droplet' do
    subject do
      promise = staging_task.promise_save_droplet
      promise.resolve
      promise
    end

    let(:droplet) { double(:droplet) }
    let(:droplet_sha) { Digest::SHA1.file(__FILE__).hexdigest }

    before do
      allow(staging_task.workspace).to receive(:staged_droplet_path) { __FILE__ }
      allow(bootstrap).to receive(:droplet_registry) do
        {
          droplet_sha => droplet
        }
      end
    end

    it 'saves droplet and droplet sha' do
      allow(droplet).to receive(:local_copy).and_yield(nil)
      subject
      expect(staging_task.droplet_sha1).to eq (droplet_sha)
    end
  end

  describe '#promise_copy_out_buildpack_cache' do
    subject do
      promise = staging_task.promise_copy_out_buildpack_cache
      promise.resolve
      promise
    end

    it 'should send copying out request' do
      allow(staging_task).to receive(:copy_out_request).with('/tmp/buildpack_cache.tgz', /.{5,}/)
      subject
    end
  end

  describe '#promise_task_log' do
    subject do
      promise = staging_task.promise_task_log
      promise.resolve
      promise
    end

    it 'should send copying out request' do
      allow(staging_task).to receive(:copy_out_request).with('/tmp/staged/logs/staging_task.log', /#{workspace_dir}/)
      subject
    end
  end

  describe '#promise_staging_info' do
    subject do
      promise = staging_task.promise_staging_info
      promise.resolve
      promise
    end

    it 'should send copying out request' do
      allow(staging_task).to receive(:copy_out_request).with('/tmp/staged/staging_info.yml', /#{workspace_dir}/)
      subject
    end
  end

  describe '#snapshot_attributes' do
    let(:services) {
      [
        {'credentials' => {}, 'syslog_drain_url' => 'abc'},
        {'credentials' => {}, 'syslog_drain_url' => 'def'},
      ]
    }
    let(:attributes) do
      attributes = valid_staging_attributes
      attributes['properties']['services'] = services
      attributes
    end

    it 'includes staging message' do
      expect(staging_task.snapshot_attributes['staging_message']).to eq(staging_message.to_hash)
    end

    it 'includes warden_container_path' do
      expect(staging_task.snapshot_attributes['warden_container_path']).to eq(staging_task.container.path)
    end

    it 'includes warden_job_id' do
      expect(staging_task.snapshot_attributes).to include('warden_job_id')
    end

    it 'includes syslog drain urls' do
      expect(staging_task.snapshot_attributes['syslog_drain_urls']).to eq(['abc', 'def'])
    end
  end

  describe '#warden_handle' do
    it 'gets the warden container handle' do
      expect(staging_task.container).to receive(:handle).and_return('container_handle')
      expect(staging_task.warden_handle).to eq('container_handle')
    end
  end

  def normalize_whitespace(script)
    script.gsub(/\s+/, ' ')
  end
end
