require 'test_helper'
require 'certificate_helper'
require 'stringio'
require 'aws-sdk-s3'

require 'aws/codedeploy/local/deployer'

class CodeDeployPluginCommandExecutorTest < InstanceAgentTestCase

  include InstanceAgent::Plugins::CodeDeployPlugin
      def generate_signed_message_for(map)
        message = @cert_helper.sign_message(map.to_json)
        spec = OpenStruct.new({ :payload => message })
        spec.format = "PKCS7/JSON"

        return spec
      end

      def s3_env_vars()
        return {
          "BUNDLE_BUCKET" => @s3Revision["Bucket"],
          "BUNDLE_KEY" => @s3Revision["Key"],
          "BUNDLE_VERSION" => @s3Revision["Version"],
          "BUNDLE_ETAG" => @s3Revision["Etag"]
        }
      end

      def github_env_vars()
        return {
          "BUNDLE_COMMIT" => @githubRevision["CommitId"]
        }
      end

  context 'The CodeDeploy Plugin Command Executor' do
    setup do
      @test_hook_mapping = { "BeforeBlockTraffic"=>["BeforeBlockTraffic"],
        "AfterBlockTraffic"=>["AfterBlockTraffic"],
        "ApplicationStop"=>["ApplicationStop"],
        "BeforeInstall"=>["BeforeInstall"],
        "AfterInstall"=>["AfterInstall"],
        "ApplicationStart"=>["ApplicationStart"],
        "BeforeAllowTraffic"=>["BeforeAllowTraffic"],
        "AfterAllowTraffic"=>["AfterAllowTraffic"],
        "ValidateService"=>["ValidateService"]}
      @deploy_control_client = mock
      @command_executor = InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor.new({
        :deploy_control_client => @deploy_control_client,
        :hook_mapping => @test_hook_mapping})
      @aws_region = 'us-east-1'
      @partition = 'aws'
      @domain = 'amazonaws.com'
      InstanceMetadata.stubs(:region).returns(@aws_region)
      InstanceMetadata.stubs(:partition).returns(@partition)
      InstanceMetadata.stubs(:domain).returns(@domain)
    end

    context "deployment_system method" do
      should "always return CodeDeploy" do
        assert_equal "CodeDeploy", @command_executor.deployment_system
      end
    end

    context "when executing a command" do

      setup do
        @cert_helper = CertificateHelper.new
        @deployment_id = SecureRandom.uuid
        @deployment_group_name = "TestDeploymentGroup"
        @application_name = "TestApplicationName"
        @deployment_group_id = "foo"
        @deployment_creator = "User"
        @deployment_type = "IN_PLACE"
        @s3Revision = {
          "Bucket" => "mybucket",
          "Key" => "mykey",
          "BundleType" => "tar"
        }
        @githubRevision = {
          'Account' => 'account',
          'Repository' => 'repository',
          'CommitId' => 'commitid',
        }
        @file_exists_behavior = "RETAIN"
        @agent_actions_overrides_map = {"FileExistsBehavior" => @file_exists_behavior}
        @agent_actions_overrides = {"AgentOverrides" => @agent_actions_overrides_map}
        @deployment_spec = generate_signed_message_for({
          "DeploymentId" => @deployment_id.to_s,
          "DeploymentGroupId" => @deployment_group_id,
          "ApplicationName" => @application_name,
          "DeploymentGroupName" => @deployment_group_name,
          "DeploymentCreator" => @deployment_creator,
          "DeploymentType" => @deployment_type,
          "AgentActionOverrides" => @agent_actions_overrides,
          "Revision" => {
            "RevisionType" => "S3",
            "S3Revision" => @s3Revision
          }
        })
        @command = Aws::CodeDeployCommand::Types::HostCommandInstance.new(
        :host_command_identifier => "command-1",
        :deployment_execution_id => "test-execution")
        @root_dir = '/tmp/codedeploy/'
        @deployment_root_dir = File.join(@root_dir, @deployment_group_id.to_s, @deployment_id.to_s)
        @deployment_instructions_dir =  File.join(@root_dir, 'deployment-instructions')
        @archive_root_dir = File.join(@deployment_root_dir, 'deployment-archive')
        ProcessManager::Config.config[:root_dir] = @root_dir

        FileUtils.stubs(:mkdir_p)
        File.stubs(:directory?).with(@deployment_root_dir).returns(true)
        @last_successful_install_file_location = File.join(@deployment_instructions_dir, "#{@deployment_group_id}_last_successful_install")
        @most_recent_install_file_location = File.join(@deployment_instructions_dir, "#{@deployment_group_id}_most_recent_install")
      end

      context "when executing an unknown command" do
        setup do
          @command.command_name = "unknown-command"
        end

        should "not create the deployment root directory" do
          # Need to unstub the :mkdir_p method otherwise the never expectation doesn't work
          FileUtils.unstub(:mkdir_p)
          FileUtils.expects(:mkdir_p).never

          assert_raised_with_message('Unsupported command type: unknown-command.', InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor::InvalidCommandNameFailure) do
            @command_executor.execute_command(@command, @deployment_spec)
          end
        end

        should "throw an exception" do
          assert_raised_with_message('Unsupported command type: unknown-command.', InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor::InvalidCommandNameFailure) do
            @command_executor.execute_command(@command, @deployment_spec)
          end
        end

        should "be a noop" do
          assert_true @command_executor.is_command_noop?(@command.command_name, @deployment_spec)
        end

        should "have a total timeout of nil" do
          assert_nil(
            @command_executor.total_timeout_for_all_lifecycle_events(@command.command_name, @deployment_spec),
            "Unknown command should have a total timeout of nil")
        end
      end

      context "when executing a valid non-hardcoded command" do
        setup do
          @command.command_name = "ValidateService"
          @command_executor.stubs(:validate_service)

          @app_spec = mock("parsed application specification")
          File.stubs(:exist?).with("#@archive_root_dir/appspec.yml").returns(true)
          File.stubs(:read).with("#@archive_root_dir/appspec.yml").returns("APP SPEC")
          ApplicationSpecification::ApplicationSpecification.stubs(:parse).with("APP SPEC").returns(@app_spec)
        end

        should "create the deployment root directory" do
          FileUtils.expects(:mkdir_p).with(@deployment_root_dir)

          @command_executor.execute_command(@command, @deployment_spec)
        end

        context "when the bundle is from github" do
          setup do
            @deployment_spec = generate_signed_message_for({
              "DeploymentId" => @deployment_id.to_s,
              "DeploymentGroupId" => @deployment_group_id.to_s,
              "ApplicationName" => @application_name,
              "DeploymentCreator" => @deployment_creator,
              "DeploymentGroupName" => @deployment_group_name,
              "Revision" => {
                "RevisionType" => "GitHub",
                "GitHubRevision" => @githubRevision
              }
            })

            @hook_executor_constructor_hash = {
              :lifecycle_event => @command.command_name,
              :application_name => @application_name,
              :deployment_id => @deployment_id,
              :deployment_group_name => @deployment_group_name,
              :deployment_group_id => @deployment_group_id,
              :deployment_creator => @deployment_creator,
              :deployment_type => @deployment_type,
              :deployment_root_dir => @deployment_root_dir,
              :last_successful_deployment_dir => nil,
              :most_recent_deployment_dir => nil,
              :app_spec_path => 'appspec.yml',
              :revision_envs => github_env_vars()}
            @mock_hook_executor = mock
            @command_executor.unstub(:validate_service)
            @command_executor.stubs(:last_successful_deployment_dir).returns(nil)
            @command_executor.stubs(:most_recent_deployment_dir).returns(nil)
          end

          should "create a hook executor with the commit hash as an environment variable" do
            HookExecutor.expects(:new).with(@hook_executor_constructor_hash).returns(@mock_hook_executor)
            @mock_hook_executor.expects(:execute)

            @command_executor.execute_command(@command, @deployment_spec)
          end
        end

        context "when failed to create root directory" do
          setup do
            File.stubs(:directory?).with(@deployment_root_dir).returns(false)
          end

          should "raise an exception" do
            assert_raised_with_message("Error creating deployment root directory #{@deployment_root_dir}") do
              @command_executor.execute_command(@command, @deployment_spec)
            end
          end
        end
      end

      context "when executing the Install command" do

        setup do
          @command.command_name = "Install"
          InstanceAgent::Plugins::CodeDeployPlugin::ApplicationSpecification::ApplicationSpecification.stubs(:parse).returns(@app_spec)
          @installer = stub("installer", :install => nil)
          Installer.stubs(:new).returns(@installer)
          File.stubs(:directory?).with(@deployment_instructions_dir).returns(true)
          File.stubs(:exist?).with(@last_successful_install_file_location).returns(true)
          File.stubs(:exist?).with(@archive_root_dir).returns(true)
          File.stubs(:open).with(@last_successful_install_file_location, 'w+')
          File.stubs(:open).with(@last_successful_install_file_location)

          @app_spec = mock("parsed application specification")
          File.stubs(:exist?).with("#@archive_root_dir/appspec.yml").returns(true)
          File.stubs(:read).with("#@archive_root_dir/appspec.yml").returns("APP SPEC")
          ApplicationSpecification::ApplicationSpecification.stubs(:parse).with("APP SPEC").returns(@app_spec)
        end

        should "not be a noop command" do
          assert_false @command_executor.is_command_noop?(@command.command_name, @deployment_spec)
        end

        should "create an appropriate Installer" do
          Installer.
          expects(:new).
          with(:deployment_instructions_dir => @deployment_instructions_dir,
          :deployment_archive_dir => @archive_root_dir,
          :file_exists_behavior => @file_exists_behavior).
          returns(@installer)

          @command_executor.execute_command(@command, @deployment_spec)
        end

        should "perform the installation for the current IG, revision and app spec" do
          @installer.expects(:install).with(@deployment_group_id, @app_spec)

          @command_executor.execute_command(@command, @deployment_spec)
        end

        should "write the archive root dir to the install instructions file" do
          mock_file = mock
          File.expects(:open).with(@last_successful_install_file_location, 'w+').yields(mock_file)
          mock_file.expects(:write).with(@deployment_root_dir)

          @command_executor.execute_command(@command, @deployment_spec)
        end

        should 'raise ArgumentError if appspec contains unknown hook and deployment_spec includes all_possible_lifecycle_events' do
          all_possible_lifecycle_events = ['ExampleLifecycleEvent', 'SecondLifecycleEvent']
          deployment_spec = generate_signed_message_for({
            "DeploymentId" => @deployment_id.to_s,
            "DeploymentGroupId" => @deployment_group_id,
            "ApplicationName" => @application_name,
            "DeploymentGroupName" => @deployment_group_name,
            "DeploymentCreator" => @deployment_creator,
            "DeploymentType" => @deployment_type,
            "AgentActionOverrides" => @agent_actions_overrides,
            "AllPossibleLifecycleEvents" => all_possible_lifecycle_events,
            "Revision" => {
              "RevisionType" => "S3",
              "S3Revision" => @s3Revision
            }
          })

          app_spec = mock("parsed application specification")
          app_spec_hooks = {'UnknownHook' => nil}
          app_spec.expects(:hooks).returns(app_spec_hooks)
          File.stubs(:read).with("#@archive_root_dir/appspec.yml").returns("APP SPEC")
          ApplicationSpecification::ApplicationSpecification.stubs(:parse).with("APP SPEC").returns(app_spec)
          unknown_hooks = app_spec_hooks.merge(@test_hook_mapping)
          assert_raised_with_message("appspec.yml file contains unknown lifecycle events: #{unknown_hooks.keys}", ArgumentError) do
            @command_executor.execute_command(@command, deployment_spec)
          end
        end

        should 'raise ArgumentError if appspec custom hook specified that does not exist in appspec' do
          all_possible_lifecycle_events = AWS::CodeDeploy::Local::Deployer::DEFAULT_ORDERED_LIFECYCLE_EVENTS + ['ExampleLifecycleEvent', 'SecondLifecycleEvent', 'CustomHookNotInAppspec']
          deployment_spec = generate_signed_message_for({
            "DeploymentId" => @deployment_id.to_s,
            "DeploymentGroupId" => @deployment_group_id,
            "ApplicationName" => @application_name,
            "DeploymentGroupName" => @deployment_group_name,
            "DeploymentCreator" => @deployment_creator,
            "DeploymentType" => @deployment_type,
            "AgentActionOverrides" => @agent_actions_overrides,
            "AllPossibleLifecycleEvents" => all_possible_lifecycle_events,
            "Revision" => {
              "RevisionType" => "S3",
              "S3Revision" => @s3Revision
            }
          })

          app_spec = mock("parsed application specification")
          app_spec_hooks = InstanceAgent::Plugins::CodeDeployPlugin::CommandPoller::DEFAULT_HOOK_MAPPING.merge({'ExampleLifecycleEvent' => nil, 'SecondLifecycleEvent' => nil})
          app_spec.expects(:hooks).twice.returns(app_spec_hooks)
          File.stubs(:read).with("#@archive_root_dir/appspec.yml").returns("APP SPEC")
          ApplicationSpecification::ApplicationSpecification.stubs(:parse).with("APP SPEC").returns(app_spec)
          assert_raised_with_message("You specified a lifecycle event which is not a default one and doesn't exist in your appspec.yml file: CustomHookNotInAppspec", ArgumentError) do
            @command_executor.execute_command(@command, deployment_spec)
          end
        end

        should 'honor the AppSpecFilename command variable' do
          deployment_spec = generate_signed_message_for({
            "DeploymentId" => @deployment_id.to_s,
            "DeploymentGroupId" => @deployment_group_id,
            "ApplicationName" => @application_name,
            "DeploymentGroupName" => @deployment_group_name,
            "DeploymentCreator" => @deployment_creator,
            "DeploymentType" => @deployment_type,
            "AgentActionOverrides" => @agent_actions_overrides,
            "AppSpecFilename" => "appspec-override.yaml",
            "Revision" => {
               "RevisionType" => "S3",
               "S3Revision" => @s3Revision
              }
            })

            File.expects(:exist?).with("#@archive_root_dir/appspec-override.yaml").returns(true)
            File.expects(:read).with("#@archive_root_dir/appspec-override.yaml").returns("APP SPEC")
            @command_executor.execute_command(@command, deployment_spec)
        end

        should 'fallback to appspec.yaml if provided AppSpecFilename variable points to a file which does\'t exist' do
          deployment_spec = generate_signed_message_for({
            "DeploymentId" => @deployment_id.to_s,
            "DeploymentGroupId" => @deployment_group_id,
            "ApplicationName" => @application_name,
            "DeploymentGroupName" => @deployment_group_name,
            "DeploymentCreator" => @deployment_creator,
            "DeploymentType" => @deployment_type,
            "AgentActionOverrides" => @agent_actions_overrides,
            "AppSpecFilename" => "appspec-override.yaml",
            "Revision" => {
              "RevisionType" => "S3",
              "S3Revision" => @s3Revision
            }
          })

          File.expects(:exist?).with("#@archive_root_dir/appspec-override.yaml").returns(false)
          File.expects(:exist?).with("#@archive_root_dir/appspec.yaml").returns(true)
          File.expects(:read).with("#@archive_root_dir/appspec.yaml").returns("APP SPEC")
          @command_executor.execute_command(@command, deployment_spec)
        end
        should 'fallback to appspec.yaml if both AppSpecFilename variable and appspec.yaml don\'t exist' do
          deployment_spec = generate_signed_message_for({
            "DeploymentId" => @deployment_id.to_s,
            "DeploymentGroupId" => @deployment_group_id,
            "ApplicationName" => @application_name,
            "DeploymentGroupName" => @deployment_group_name,
            "DeploymentCreator" => @deployment_creator,
            "DeploymentType" => @deployment_type,
            "AgentActionOverrides" => @agent_actions_overrides,
            "AppSpecFilename" => "appspec-override.yaml",
            "Revision" => {
              "RevisionType" => "S3",
              "S3Revision" => @s3Revision
            }
          })

          File.expects(:exist?).with("#@archive_root_dir/appspec-override.yaml").returns(false)
          File.expects(:exist?).with("#@archive_root_dir/appspec.yaml").returns(false)
          File.expects(:read).with("#@archive_root_dir/appspec.yml").returns("APP SPEC")
          @command_executor.execute_command(@command, deployment_spec)
        end

        should 'fallback to appspec.yaml if appspec.yml is not there and no AppSpecFilename arg is specified' do
          deployment_spec = generate_signed_message_for({
            "DeploymentId" => @deployment_id.to_s,
            "DeploymentGroupId" => @deployment_group_id,
            "ApplicationName" => @application_name,
            "DeploymentGroupName" => @deployment_group_name,
            "DeploymentCreator" => @deployment_creator,
            "DeploymentType" => @deployment_type,
            "AgentActionOverrides" => @agent_actions_overrides,
            "Revision" => {
              "RevisionType" => "S3",
              "S3Revision" => @s3Revision
            }
          })

          File.expects(:exist?).with("#@archive_root_dir/appspec.yml").returns(false)
          File.expects(:exist?).with("#@archive_root_dir/appspec.yaml").returns(true)
          File.expects(:read).with("#@archive_root_dir/appspec.yaml").returns("APP SPEC")
          @command_executor.execute_command(@command, deployment_spec)
        end
      end

      context "when executing the DownloadBundle command" do
        setup do
          InstanceAgent::LinuxUtil.stubs(:extract_tar)
          InstanceAgent::LinuxUtil.stubs(:extract_tgz)
          @command.command_name = "DownloadBundle"
          @http = mock
          @mock_file = mock
          Net::HTTP.stubs(:start).yields(@http)
          File.stubs(:open).returns @mock_file
          Dir.stubs(:entries).returns []
          @mock_file.stubs(:close)
          @http.stubs(:request_get)
          @s3 = mock
          @s3.stubs(:config).returns("hello")
          Aws::S3::Client.stubs(:new).returns(@s3)
        end

        should "not be a noop" do 
          assert_false @command_executor.is_command_noop?(@command.command_name, @deployment_spec)
        end

        context "when GitHub revision specified" do
          setup do
            File.stubs(:directory?).with(@archive_root_dir).returns(true)
            FileUtils.stubs(:mv)
            FileUtils.stubs(:rmdir)
            @mock_file.stubs(:write)
            @deployment_spec = generate_signed_message_for({
              "DeploymentId" => @deployment_id.to_s,
              "DeploymentGroupId" => @deployment_group_id.to_s,
              "ApplicationName" => @application_name,
              "DeploymentGroupName" => @deployment_group_name,
              "Revision" => {
                "RevisionType" => "GitHub",
                "GitHubRevision" => {
                  'Account' => 'account',
                  'Repository' => 'repository',
                  'CommitId' => 'commitid',
                }
              }
            })

            ENV['AWS_SSL_CA_DIRECTORY'] = 'aws_ssl_ca_directory'
            @mock_uri = mock
            uri_options = {:ssl_verify_mode => OpenSSL::SSL::VERIFY_PEER, :redirect => true, :ssl_ca_cert => ENV['AWS_SSL_CA_DIRECTORY']}
            @mock_buffer = mock
            @mock_github_response = mock
            @mock_github_response.stubs(:read).returns(@mock_buffer)
            @mock_uri.stubs(:open).with(uri_options).yields(@mock_github_response)
          end

          should 'download file from github' do
            URI.expects(:parse).with("https://api.github.com/repos/account/repository/tarball/commitid").returns(@mock_uri)
            @command_executor.execute_command(@command, @deployment_spec)
          end

          context 'when Github bundle_type is specified' do
            setup do
              @bundle_type = 'zip'

              @deployment_spec = generate_signed_message_for({
                "DeploymentId" => @deployment_id.to_s,
                "DeploymentGroupId" => @deployment_group_id.to_s,
                "ApplicationName" => @application_name,
                "DeploymentGroupName" => @deployment_group_name,
                "Revision" => {
                  "RevisionType" => "GitHub",
                  "GitHubRevision" => {
                    'Account' => 'account',
                    'Repository' => 'repository',
                    'CommitId' => 'commitid',
                    'BundleType' => @bundle_type
                  }
                }
              })
            end

            should 'downloads from github with the corresponding format' do
              URI.expects(:parse).with("https://api.github.com/repos/account/repository/zipball/commitid").returns(@mock_uri)
              Zip::File.expects(:open).with(File.join(@deployment_root_dir, 'bundle.tar'))
              @command_executor.execute_command(@command, @deployment_spec)
            end
          end
        end

        context "when creating S3 options" do
          should "use right signature version" do 
            assert_equal 'v4', @command_executor.s3_options[:signature_version]
          end

          context "when override endpoint provided" do
            setup do
              InstanceAgent::Config.config[:s3_endpoint_override] = "https://example.override.endpoint.com"
            end
            should "use the override endpoint" do
              assert_equal "https://example.override.endpoint.com", @command_executor.s3_options[:endpoint].to_s
            end
          end

          context "when no override endpoint provided and not using fips" do
            setup do
              InstanceAgent::Config.config[:s3_endpoint_override] = nil
              InstanceAgent::Config.config[:use_fips_mode] = false
            end
            should "use correct region and custom endpoint" do
              assert_equal 'us-east-1', @command_executor.s3_options[:region]
              assert_false @command_executor.s3_options.include? :endpoint
            end
          end

          context "when no override endpoint provided and using fips" do
            setup do
              InstanceAgent::Config.config[:s3_endpoint_override] = nil
              InstanceAgent::Config.config[:use_fips_mode] = true
            end
            should "use correct region and custom endpoint" do
              assert_equal 'us-east-1', @command_executor.s3_options[:region]
              assert_true @command_executor.s3_options.include? :endpoint
            end
          end
        end

        context "downloading bundle from S3" do
          setup do
            File.expects(:open).with(File.join(@deployment_root_dir, 'bundle.tar'), 'wb').yields(@mock_file)
            @object = mock
            @s3.stubs(:get_object).returns(@object)
            @io = mock
            @object.stubs(:etag).returns("myetag")
            @deployment_spec = generate_signed_message_for({
              "DeploymentId" => @deployment_id.to_s,
              "DeploymentGroupId" => @deployment_group_id.to_s,
              "ApplicationName" => @application_name,
              "DeploymentGroupName" => @deployment_group_name,
              "Revision" => {
                "RevisionType" => "S3",
                "S3Revision" => {
                  "Bucket" => "mybucket",
                  "Key" => "mykey",
                  "BundleType" => "tar"
                }
              }
            })
          end

          context "when setting up the S3 client" do
            setup do
              ENV['AWS_REGION'] = nil
              InstanceMetadata.stubs(:region).returns('us-east-1')
            end

            should "read from the InstanceMetadata to get the region" do
              InstanceMetadata.expects(:region)
              @command_executor.execute_command(@command, @deployment_spec)
            end
          end

          should "verify etag" do
            @deployment_spec = generate_signed_message_for({
              "DeploymentId" => @deployment_id.to_s,
              "DeploymentGroupId" => @deployment_group_id.to_s,
              "ApplicationName" => @application_name,
              "DeploymentGroupName" => @deployment_group_name,
              "Revision" => {
                "RevisionType" => "S3",
                "S3Revision" => {
                  "Bucket" => "mybucket",
                  "Key" => "mykey",
                  "BundleType" => "tar",
                  "ETag" => "myetag"
                }
              }
            })
            @command_executor.execute_command(@command, @deployment_spec)
          end

          should "verify etag that contains quotations still matches" do
            @object.stubs(:etag).returns("\"myetag\"")
            @deployment_spec = generate_signed_message_for({
              "DeploymentId" => @deployment_id.to_s,
              "DeploymentGroupId" => @deployment_group_id.to_s,
              "ApplicationName" => @application_name,
              "DeploymentGroupName" => @deployment_group_name,
              "Revision" => {
                "RevisionType" => "S3",
                "S3Revision" => {
                  "Bucket" => "mybucket",
                  "Key" => "mykey",
                  "BundleType" => "tar",
                  "ETag" => "myetag"
                }
              }
            })
            @command_executor.execute_command(@command, @deployment_spec)
          end

          should "verify version" do
            @object.stubs(:body).with(:bucket => "mybucket", :key => "mykey", :version_id => "myversion").returns(@io)
            @deployment_spec = generate_signed_message_for({
              "DeploymentId" => @deployment_id.to_s,
              "DeploymentGroupId" => @deployment_group_id.to_s,
              "ApplicationName" => @application_name,
              "DeploymentGroupName" => @deployment_group_name,
              "Revision" => {
                "RevisionType" => "S3",
                "S3Revision" => {
                  "Bucket" => "mybucket",
                  "Key" => "mykey",
                  "BundleType" => "tar",
                  "Version" => "myversion"
                }
              }
            })
            @command_executor.execute_command(@command, @deployment_spec)
          end

          should "call zip for zip BundleTypes" do
            @deployment_spec = generate_signed_message_for({
              "DeploymentId" => @deployment_id.to_s,
              "DeploymentGroupId" => @deployment_group_id.to_s,
              "ApplicationName" => @application_name,
              "DeploymentGroupName" => @deployment_group_name,
              "Revision" => {
                "RevisionType" => "S3",
                "S3Revision" => {
                  "Bucket" => "mybucket",
                  "Key" => "mykey",
                  "BundleType" => "zip"
                }
              }
            })
            Zip::File.expects(:open).with(File.join(@deployment_root_dir, 'bundle.tar'))
            @command_executor.execute_command(@command, @deployment_spec)
          end

          should "call extract_tgz for Gzipped tar BundleTypes" do
            @deployment_spec = generate_signed_message_for({
              "DeploymentId" => @deployment_id.to_s,
              "DeploymentGroupId" => @deployment_group_id.to_s,
              "ApplicationName" => @application_name,
              "DeploymentGroupName" => @deployment_group_name,
              "Revision" => {
                "RevisionType" => "S3",
                "S3Revision" => {
                  "Bucket" => "mybucket",
                  "Key" => "mykey",
                  "BundleType" => "tgz"
                }
              }
            })
            InstanceAgent::LinuxUtil.expects(:extract_tgz).with(File.join(@deployment_root_dir, 'bundle.tar'), @archive_root_dir)
            @command_executor.execute_command(@command, @deployment_spec)
          end

          should "call extract_tar for tar BundleTypes" do
            @deployment_spec = generate_signed_message_for({
              "DeploymentId" => @deployment_id.to_s,
              "DeploymentGroupId" => @deployment_group_id.to_s,
              "ApplicationName" => @application_name,
              "DeploymentGroupName" => @deployment_group_name,
              "Revision" => {
                "RevisionType" => "S3",
                "S3Revision" => {
                  "Bucket" => "mybucket",
                  "Key" => "mykey",
                  "BundleType" => "tar"
                }
              }
            })
            InstanceAgent::LinuxUtil.expects(:extract_tar).with(File.join(@deployment_root_dir, 'bundle.tar'), @archive_root_dir)
            @command_executor.execute_command(@command, @deployment_spec)
          end

        end

        context "extract bundle from local file" do
          setup do
            InstanceAgent::LinuxUtil.stubs(:extract_tgz)
            @command.command_name = "DownloadBundle"
            @mock_file = mock
            @mock_file_location = '/mock/file/location.tgz'
            File.stubs(:symlink)
            Dir.stubs(:entries).returns []
            @mock_file.stubs(:close)

            @deployment_spec = generate_signed_message_for({
              "DeploymentId" => @deployment_id.to_s,
              "DeploymentGroupId" => @deployment_group_id.to_s,
              "ApplicationName" => @application_name,
              "DeploymentGroupName" => @deployment_group_name,
              "Revision" => {
                "RevisionType" => "Local File",
                "LocalRevision" => {
                  "Location" => @mock_file_location,
                  "BundleType" => 'tgz'
                }
              }
            })
          end

          should 'symlink the file to the bundle location' do
            File.expects(:symlink).with(@mock_file_location, File.join(@deployment_root_dir, 'bundle.tar'))
            @command_executor.execute_command(@command, @deployment_spec)
          end
        end
        
        context "handle bundle from local directory" do
          setup do
            @command.command_name = "DownloadBundle"
            @mock_directory_location = '/mock/directory/location/'
            FileUtils.stubs(:cp_r)
            Dir.stubs(:entries).returns []
            @mock_file.stubs(:close)

            @deployment_spec = generate_signed_message_for({
              "DeploymentId" => @deployment_id.to_s,
              "DeploymentGroupId" => @deployment_group_id.to_s,
              "ApplicationName" => @application_name,
              "DeploymentGroupName" => @deployment_group_name,
              "Revision" => {
                "RevisionType" => "Local Directory",
                "LocalRevision" => {
                  "Location" => @mock_directory_location,
                  "BundleType" => 'directory'
                }
              }
            })
          end

          should 'copy recursively the directory to the bundle location' do
            FileUtils.expects(:cp_r).with(@mock_directory_location, @archive_root_dir)
            @command_executor.execute_command(@command, @deployment_spec)
          end
        end

        should "unpack the bundle to the right directory" do
          InstanceAgent::LinuxUtil.expects(:extract_tar).with(File.join(@deployment_root_dir, 'bundle.tar'), @archive_root_dir)
          @command_executor.execute_command(@command, @deployment_spec)
        end

        should "remove the directory before unpacking" do
          call_sequence = sequence("call sequence")
          FileUtils.expects(:rm_rf).with(@archive_root_dir).in_sequence(call_sequence)
          InstanceAgent::LinuxUtil.expects(:extract_tar).in_sequence(call_sequence)
          @command_executor.execute_command(@command, @deployment_spec)
        end

        should "idempotently create the instructions directory" do
          FileUtils.expects(:mkdir_p).with(@deployment_instructions_dir)

          @command_executor.execute_command(@command, @deployment_spec)
        end

        should "write the archive root dir to the install instructions file" do
          mock_file = mock
          File.expects(:open).with(@most_recent_install_file_location, 'w+').yields(mock_file)
          mock_file.expects(:write).with(@deployment_root_dir)

          @command_executor.execute_command(@command, @deployment_spec)
        end
      end

      context "I have an empty app spec (for script mapping)" do
        setup do
          File.stubs(:read).with(File.join(@archive_root_dir, 'appspec.yml')).returns(nil)
          InstanceAgent::Plugins::CodeDeployPlugin::ApplicationSpecification::ApplicationSpecification.stubs(:parse).returns(nil)
          @hook_executor_constructor_hash = {
            :application_name => @application_name,
            :deployment_id => @deployment_id,
            :deployment_group_name => @deployment_group_name,
            :deployment_group_id => @deployment_group_id,
            :deployment_creator => @deployment_creator,
            :deployment_type => @deployment_type,
            :deployment_root_dir => @deployment_root_dir,
            :last_successful_deployment_dir => nil,
            :most_recent_deployment_dir => nil,
            :app_spec_path => 'appspec.yml',
            :revision_envs => s3_env_vars()}
          @mock_hook_executor = mock
        end

        context "BeforeBlockTraffic" do
          setup do
            @command.command_name = "BeforeBlockTraffic"
            @hook_executor_constructor_hash[:lifecycle_event] = "BeforeBlockTraffic"
          end

          should "call execute a hook executor object with BeforeBlockTraffic as one of the params" do
            HookExecutor.expects(:new).with(@hook_executor_constructor_hash).returns(@mock_hook_executor)
            @mock_hook_executor.expects(:execute)
            @command_executor.execute_command(@command, @deployment_spec)
          end

          should "be a noop" do
            HookExecutor.expects(:new).with(@hook_executor_constructor_hash).returns(@mock_hook_executor)
            @mock_hook_executor.expects(:is_noop?).returns(true)
            assert_true @command_executor.is_command_noop?(@command.command_name, @deployment_spec)
          end

          should "have a total script timeout of nil" do
            assert_nil(
              @command_executor.total_timeout_for_all_lifecycle_events(@command, @deployment_spec),
              "Total timeout should be whatever's returned by HookExecutor")
          end
        end

        context "AfterBlockTraffic" do
          setup do
            @command.command_name = "AfterBlockTraffic"
            @hook_executor_constructor_hash[:lifecycle_event] = "AfterBlockTraffic"
          end

          should "call execute a hook executor object with AfterBlockTraffic as one of the params" do
            HookExecutor.expects(:new).with(@hook_executor_constructor_hash).returns(@mock_hook_executor)
            @mock_hook_executor.expects(:execute)
            @command_executor.execute_command(@command, @deployment_spec)
          end

          should "be a noop" do
            HookExecutor.expects(:new).with(@hook_executor_constructor_hash).returns(@mock_hook_executor)
            @mock_hook_executor.expects(:is_noop?).returns(true)
            assert_true @command_executor.is_command_noop?(@command.command_name, @deployment_spec)
          end
        end

        context "ApplicationStop" do
          setup do
            @command.command_name = "ApplicationStop"
            @hook_executor_constructor_hash[:lifecycle_event] = "ApplicationStop"
          end

          should "call execute a hook executor object with ApplicationStop as one of the params" do
            HookExecutor.expects(:new).with(@hook_executor_constructor_hash).returns(@mock_hook_executor)
            @mock_hook_executor.expects(:execute)
            @command_executor.execute_command(@command, @deployment_spec)
          end

          should "be a noop" do
            HookExecutor.expects(:new).with(@hook_executor_constructor_hash).returns(@mock_hook_executor)
            @mock_hook_executor.expects(:is_noop?).returns(true)
            assert_true @command_executor.is_command_noop?(@command.command_name, @deployment_spec)
          end
        end

        context "BeforeInstall" do
          setup do
            @command.command_name = "BeforeInstall"
            @hook_executor_constructor_hash[:lifecycle_event] = "BeforeInstall"
          end

          should "call execute a hook executor object with BeforeInstall as one of the params" do
            HookExecutor.expects(:new).with(@hook_executor_constructor_hash).returns(@mock_hook_executor)
            @mock_hook_executor.expects(:execute)
            @command_executor.execute_command(@command, @deployment_spec)
          end

          should "be a noop" do
            HookExecutor.expects(:new).with(@hook_executor_constructor_hash).returns(@mock_hook_executor)
            @mock_hook_executor.expects(:is_noop?).returns(true)
            assert_true @command_executor.is_command_noop?(@command.command_name, @deployment_spec)
          end
        end

        context "AfterInstall" do
          setup do
            @command.command_name = "AfterInstall"
            @hook_executor_constructor_hash[:lifecycle_event] = "AfterInstall"
          end

          should "call execute a hook executor object with AfterInstall as one of the params" do
            HookExecutor.expects(:new).with(@hook_executor_constructor_hash).returns(@mock_hook_executor)
            @mock_hook_executor.expects(:execute)
            @command_executor.execute_command(@command, @deployment_spec)
          end

          should "be a noop" do
            HookExecutor.expects(:new).with(@hook_executor_constructor_hash).returns(@mock_hook_executor)
            @mock_hook_executor.expects(:is_noop?).returns(true)
            assert_true @command_executor.is_command_noop?(@command.command_name, @deployment_spec)
          end
        end

        context "ApplicationStart" do
          setup do
            @command.command_name = "ApplicationStart"
            @hook_executor_constructor_hash[:lifecycle_event] = "ApplicationStart"
          end

          should "call execute a hook executor object with ApplicationStart as one of the params" do
            HookExecutor.expects(:new).with(@hook_executor_constructor_hash).returns(@mock_hook_executor)
            @mock_hook_executor.expects(:execute)
            @command_executor.execute_command(@command, @deployment_spec)
          end

          should "be a noop" do
            HookExecutor.expects(:new).with(@hook_executor_constructor_hash).returns(@mock_hook_executor)
            @mock_hook_executor.expects(:is_noop?).returns(true)
            assert_true @command_executor.is_command_noop?(@command.command_name, @deployment_spec)
          end
        end

        context "BeforeAllowTraffic" do
          setup do
            @command.command_name = "BeforeAllowTraffic"
            @hook_executor_constructor_hash[:lifecycle_event] = "BeforeAllowTraffic"
          end

          should "call execute a hook executor object with BeforeAllowTraffic as one of the params" do
            HookExecutor.expects(:new).with(@hook_executor_constructor_hash).returns(@mock_hook_executor)
            @mock_hook_executor.expects(:execute)
            @command_executor.execute_command(@command, @deployment_spec)
          end

          should "be a noop" do
            HookExecutor.expects(:new).with(@hook_executor_constructor_hash).returns(@mock_hook_executor)
            @mock_hook_executor.expects(:is_noop?).returns(true)
            assert_true @command_executor.is_command_noop?(@command.command_name, @deployment_spec)
          end
        end

        context "AfterAllowTraffic" do
          setup do
            @command.command_name = "AfterAllowTraffic"
            @hook_executor_constructor_hash[:lifecycle_event] = "AfterAllowTraffic"
          end

          should "call execute a hook executor object with AfterAllowTraffic as one of the params" do
            HookExecutor.expects(:new).with(@hook_executor_constructor_hash).returns(@mock_hook_executor)
            @mock_hook_executor.expects(:execute)
            @command_executor.execute_command(@command, @deployment_spec)
          end

          should "be a noop" do
            HookExecutor.expects(:new).with(@hook_executor_constructor_hash).returns(@mock_hook_executor)
            @mock_hook_executor.expects(:is_noop?).returns(true)
            assert_true @command_executor.is_command_noop?(@command.command_name, @deployment_spec)
          end
        end

        context "ValidateService" do
          setup do
            @command.command_name = "ValidateService"
            @hook_executor_constructor_hash[:lifecycle_event] = "ValidateService"
          end

          should "call execute a hook executor object with ValidateService as one of the params" do
            HookExecutor.expects(:new).with(@hook_executor_constructor_hash).returns(@mock_hook_executor)
            @mock_hook_executor.expects(:execute)
            @command_executor.execute_command(@command, @deployment_spec)
          end

          should "be a noop" do
            HookExecutor.expects(:new).with(@hook_executor_constructor_hash).returns(@mock_hook_executor)
            @mock_hook_executor.expects(:is_noop?).returns(true)
            assert_true @command_executor.is_command_noop?(@command.command_name, @deployment_spec)
          end
        end
      end

      #non 1:1 mapping tests
      context "one command hooks to multiple lifecycle events" do
        setup do
          @command.command_name = "TestCommand"
          @test_hook_mapping = { "TestCommand" => ["lifecycle_event_1","lifecycle_event_2"]}
          @deploy_control_client = mock
          @command_executor = InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor.new({
            :deploy_control_client => @deploy_control_client,
            :hook_mapping => @test_hook_mapping})
          hook_executor_constructor_hash = {
            :deployment_root_dir => @deployment_root_dir,
            :application_name => @application_name,
            :deployment_id => @deployment_id,
            :deployment_group_name => @deployment_group_name,
            :deployment_group_id => @deployment_group_id,
            :deployment_creator => @deployment_creator,
            :deployment_type => @deployment_type,
            :last_successful_deployment_dir => nil,
            :most_recent_deployment_dir => nil,
            :app_spec_path => 'appspec.yml',
            :revision_envs => s3_env_vars()}
          @hook_executor_constructor_hash_1 = hook_executor_constructor_hash.merge({:lifecycle_event => "lifecycle_event_1"})
          @hook_executor_constructor_hash_2 = hook_executor_constructor_hash.merge({:lifecycle_event => "lifecycle_event_2"})
          @mock_hook_executor = mock
        end

        should "call both lifecycle events" do
          HookExecutor.expects(:new).with(@hook_executor_constructor_hash_1).returns(@mock_hook_executor)
          HookExecutor.expects(:new).with(@hook_executor_constructor_hash_2).returns(@mock_hook_executor)
          @mock_hook_executor.expects(:execute).twice

          @command_executor.execute_command(@command, @deployment_spec)
        end

        should "not be a noop" do
          HookExecutor.expects(:new).with(@hook_executor_constructor_hash_1).returns(@mock_hook_executor)
          HookExecutor.expects(:new).with(@hook_executor_constructor_hash_2).returns(@mock_hook_executor)

          @mock_hook_executor.expects(:is_noop?).twice.returns(true, false)

          assert_false @command_executor.is_command_noop?(@command.command_name, @deployment_spec)
        end

        should "have a total timeout of 900 seconds" do
          HookExecutor.expects(:new).with(@hook_executor_constructor_hash_1).returns(@mock_hook_executor)
          HookExecutor.expects(:new).with(@hook_executor_constructor_hash_2).returns(@mock_hook_executor)

          @mock_hook_executor.expects(:total_timeout_for_all_scripts).twice.returns(300, 600)

          assert_equal(
            900,
            @command_executor.total_timeout_for_all_lifecycle_events(@command.command_name, @deployment_spec),
            "Timeout should be the sum of the appspec timeouts for the scripts for all lifecycle events"
          )
        end

        should "have a total timeout of nil when one command has no timeout" do
          HookExecutor.expects(:new).with(@hook_executor_constructor_hash_1).returns(@mock_hook_executor)
          HookExecutor.expects(:new).with(@hook_executor_constructor_hash_2).returns(@mock_hook_executor)

          @mock_hook_executor.expects(:total_timeout_for_all_scripts).twice.returns(600, nil)

          assert_nil(
            @command_executor.total_timeout_for_all_lifecycle_events(@command.command_name, @deployment_spec),
            "Timeout should be nil if any script's timeout is nil")
        end

        context "when the first script is forced to fail" do
          setup do
            HookExecutor.stubs(:new).with(@hook_executor_constructor_hash_1).raises("failed to create hook command")
          end

          should "calls lifecycle event 1 and fails but not lifecycle event 2" do
            assert_raised_with_message('failed to create hook command') do
              @command_executor.execute_command(@command, @deployment_spec)
            end
            HookExecutor.expects(:new).with(@hook_executor_constructor_hash_2).never
          end
        end
      end
    end
  end
end
