import os
import random
import string
import tempfile
import zipfile
from contextlib import closing, contextmanager

import botocore.session
import pytest
from botocore.stub import Stubber

from awscli.customizations.cloudformation import exceptions
from awscli.customizations.cloudformation.artifact_exporter import (
    GLOBAL_EXPORT_DICT,
    ApiGatewayRestApiResource,
    AppSyncFunctionConfigurationRequestTemplateResource,
    AppSyncFunctionConfigurationResponseTemplateResource,
    AppSyncResolverRequestTemplateResource,
    AppSyncResolverResponseTemplateResource,
    CloudFormationStackResource,
    CodeCommitRepositoryS3Resource,
    ElasticBeanstalkApplicationVersion,
    GlueJobCommandScriptLocationResource,
    GraphQLSchemaResource,
    LambdaFunctionResource,
    LambdaLayerVersionResource,
    Resource,
    ResourceWithS3UrlDict,
    ServerlessApiResource,
    ServerlessApplicationResource,
    ServerlessFunctionResource,
    ServerlessLayerVersionResource,
    ServerlessRepoApplicationLicense,
    ServerlessRepoApplicationReadme,
    ServerlessStateMachineDefinitionResource,
    StepFunctionsStateMachineDefinitionResource,
    Template,
    copy_to_temp_dir,
    include_transform_export_handler,
    is_local_file,
    is_local_folder,
    is_s3_url,
    make_abs_path,
    make_zip,
    parse_s3_url,
    upload_local_artifacts,
    zip_folder,
)
from awscli.testutils import FileCreator, mock
from tests.unit.customizations.cloudformation import BaseYAMLTest

VALID_CASES = [
    "s3://foo/bar",
    "s3://foo/bar/baz/cat/dog",
    "s3://foo/bar?versionId=abc",
    "s3://foo/bar/baz?versionId=abc&versionId=123",
    "s3://foo/bar/baz?versionId=abc",
    "s3://www.amazon.com/foo/bar",
    "s3://my-new-bucket/foo/bar?a=1&a=2&a=3&b=1",
]

INVALID_CASES = [
    # For purposes of exporter, we need S3 URLs to point to an object
    # and not a bucket
    "s3://foo",
    # two versionIds is invalid
    "https://s3-eu-west-1.amazonaws.com/bucket/key",
    "https://www.amazon.com",
]


@pytest.mark.parametrize("url", VALID_CASES)
def test_is_valid_s3_url(url):
    assert is_s3_url(url), f"{url} should be valid"


@pytest.mark.parametrize("url", INVALID_CASES)
def test_is_invalid_s3_url(url):
    assert not is_s3_url(url), f"{url} should be invalid"


UPLOADED_S3_URL = "s3://foo/bar?versionId=baz"

RESOURCE_EXPORT_TEST_CASES = [
    {"class": ServerlessFunctionResource, "expected_result": UPLOADED_S3_URL},
    {"class": ServerlessApiResource, "expected_result": UPLOADED_S3_URL},
    {"class": GraphQLSchemaResource, "expected_result": UPLOADED_S3_URL},
    {
        "class": AppSyncResolverRequestTemplateResource,
        "expected_result": UPLOADED_S3_URL,
    },
    {
        "class": AppSyncResolverResponseTemplateResource,
        "expected_result": UPLOADED_S3_URL,
    },
    {
        "class": AppSyncFunctionConfigurationRequestTemplateResource,
        "expected_result": UPLOADED_S3_URL,
    },
    {
        "class": AppSyncFunctionConfigurationResponseTemplateResource,
        "expected_result": UPLOADED_S3_URL,
    },
    {
        "class": ApiGatewayRestApiResource,
        "expected_result": {"Bucket": "foo", "Key": "bar", "Version": "baz"},
    },
    {
        "class": LambdaFunctionResource,
        "expected_result": {
            "S3Bucket": "foo",
            "S3Key": "bar",
            "S3ObjectVersion": "baz",
        },
    },
    {
        "class": ElasticBeanstalkApplicationVersion,
        "expected_result": {"S3Bucket": "foo", "S3Key": "bar"},
    },
    {
        "class": LambdaLayerVersionResource,
        "expected_result": {
            "S3Bucket": "foo",
            "S3Key": "bar",
            "S3ObjectVersion": "baz",
        },
    },
    {
        "class": ServerlessLayerVersionResource,
        "expected_result": UPLOADED_S3_URL,
    },
    {
        "class": ServerlessRepoApplicationReadme,
        "expected_result": UPLOADED_S3_URL,
    },
    {
        "class": ServerlessRepoApplicationLicense,
        "expected_result": UPLOADED_S3_URL,
    },
    {
        "class": ServerlessRepoApplicationLicense,
        "expected_result": UPLOADED_S3_URL,
    },
    {
        "class": GlueJobCommandScriptLocationResource,
        "expected_result": {"ScriptLocation": UPLOADED_S3_URL},
    },
    {
        "class": StepFunctionsStateMachineDefinitionResource,
        "expected_result": {"Bucket": "foo", "Key": "bar", "Version": "baz"},
    },
    {
        "class": ServerlessStateMachineDefinitionResource,
        "expected_result": {"Bucket": "foo", "Key": "bar", "Version": "baz"},
    },
]


@pytest.mark.parametrize("test", RESOURCE_EXPORT_TEST_CASES)
def test_all_resources_export(test):
    mock_path = "awscli.customizations.cloudformation.artifact_exporter.upload_local_artifacts"
    with mock.patch(mock_path) as upload_local_artifacts_mock:
        _helper_verify_export_resources(
            test["class"], upload_local_artifacts_mock, test["expected_result"]
        )


def _helper_verify_export_resources(
    test_class, upload_local_artifacts_mock, expected_result
):
    s3_uploader_mock = mock.Mock()
    upload_local_artifacts_mock.reset_mock()

    resource_id = "id"
    parent_dir = "dir"

    if '.' in test_class.PROPERTY_NAME:
        reversed_property_names = test_class.PROPERTY_NAME.split('.')
        reversed_property_names.reverse()
        property_dict = {reversed_property_names[0]: "foo"}
        for sub_property_name in reversed_property_names[1:]:
            property_dict = {sub_property_name: property_dict}
        resource_dict = property_dict
    else:
        resource_dict = {test_class.PROPERTY_NAME: "foo"}

    upload_local_artifacts_mock.return_value = UPLOADED_S3_URL
    resource_obj = test_class(s3_uploader_mock)
    resource_obj.export(resource_id, resource_dict, parent_dir)

    upload_local_artifacts_mock.assert_called_once_with(
        resource_id,
        resource_dict,
        test_class.PROPERTY_NAME,
        parent_dir,
        s3_uploader_mock,
    )
    if '.' in test_class.PROPERTY_NAME:
        top_level_property_name = test_class.PROPERTY_NAME.split('.')[0]
        result = resource_dict[top_level_property_name]
    else:
        result = resource_dict[test_class.PROPERTY_NAME]

    assert result == expected_result


class TestArtifactExporter(BaseYAMLTest):
    def setUp(self):
        super(TestArtifactExporter, self).setUp()
        self.s3_uploader_mock = mock.Mock()
        self.s3_uploader_mock.s3.meta.endpoint_url = (
            "https://s3.some-valid-region.amazonaws.com"
        )

    def test_parse_s3_url(self):
        valid = [
            {"url": "s3://foo/bar", "result": {"Bucket": "foo", "Key": "bar"}},
            {
                "url": "s3://foo/bar/cat/dog",
                "result": {"Bucket": "foo", "Key": "bar/cat/dog"},
            },
            {
                "url": "s3://foo/bar/baz?versionId=abc&param1=val1&param2=val2",
                "result": {
                    "Bucket": "foo",
                    "Key": "bar/baz",
                    "VersionId": "abc",
                },
            },
            {
                # VersionId is not returned if there are more than one versionId
                # keys in query parameter
                "url": "s3://foo/bar/baz?versionId=abc&versionId=123",
                "result": {"Bucket": "foo", "Key": "bar/baz"},
            },
        ]

        invalid = [
            # For purposes of exporter, we need S3 URLs to point to an object
            # and not a bucket
            "s3://foo",
            # two versionIds is invalid
            "https://s3-eu-west-1.amazonaws.com/bucket/key",
            "https://www.amazon.com",
        ]

        for config in valid:
            result = parse_s3_url(
                config["url"],
                bucket_name_property="Bucket",
                object_key_property="Key",
                version_property="VersionId",
            )

            self.assertEqual(result, config["result"])

        for url in invalid:
            with self.assertRaises(ValueError):
                parse_s3_url(url)

    def test_is_local_file(self):
        with tempfile.NamedTemporaryFile() as handle:
            self.assertTrue(is_local_file(handle.name))
            self.assertFalse(is_local_folder(handle.name))

    def test_is_local_folder(self):
        with self.make_temp_dir() as filename:
            self.assertTrue(is_local_folder(filename))
            self.assertFalse(is_local_file(filename))

    @mock.patch(
        "awscli.customizations.cloudformation.artifact_exporter.zip_and_upload"
    )
    def test_upload_local_artifacts_local_file(self, zip_and_upload_mock):
        # Case 1: Artifact path is a relative path
        # Verifies that we package local artifacts appropriately
        property_name = "property"
        resource_id = "resource_id"
        expected_s3_url = "s3://foo/bar?versionId=baz"

        self.s3_uploader_mock.upload_with_dedup.return_value = expected_s3_url

        with tempfile.NamedTemporaryFile() as handle:
            # Artifact is a file in the temporary directory
            artifact_path = handle.name
            parent_dir = tempfile.gettempdir()

            resource_dict = {property_name: artifact_path}
            result = upload_local_artifacts(
                resource_id,
                resource_dict,
                property_name,
                parent_dir,
                self.s3_uploader_mock,
            )
            self.assertEqual(result, expected_s3_url)

            # Internally the method would convert relative paths to absolute
            # path, with respect to the parent directory
            absolute_artifact_path = make_abs_path(parent_dir, artifact_path)
            self.s3_uploader_mock.upload_with_dedup.assert_called_with(
                absolute_artifact_path
            )

            zip_and_upload_mock.assert_not_called()

    @mock.patch(
        "awscli.customizations.cloudformation.artifact_exporter.zip_and_upload"
    )
    def test_upload_local_artifacts_local_file_abs_path(
        self, zip_and_upload_mock
    ):
        # Case 2: Artifact path is an absolute path
        # Verifies that we package local artifacts appropriately
        property_name = "property"
        resource_id = "resource_id"
        expected_s3_url = "s3://foo/bar?versionId=baz"

        self.s3_uploader_mock.upload_with_dedup.return_value = expected_s3_url

        with tempfile.NamedTemporaryFile() as handle:
            parent_dir = tempfile.gettempdir()
            artifact_path = make_abs_path(parent_dir, handle.name)

            resource_dict = {property_name: artifact_path}
            result = upload_local_artifacts(
                resource_id,
                resource_dict,
                property_name,
                parent_dir,
                self.s3_uploader_mock,
            )
            self.assertEqual(result, expected_s3_url)

            self.s3_uploader_mock.upload_with_dedup.assert_called_with(
                artifact_path
            )
            zip_and_upload_mock.assert_not_called()

    @mock.patch(
        "awscli.customizations.cloudformation.artifact_exporter.zip_and_upload"
    )
    def test_upload_local_artifacts_local_folder(self, zip_and_upload_mock):
        property_name = "property"
        resource_id = "resource_id"
        expected_s3_url = "s3://foo/bar?versionId=baz"

        zip_and_upload_mock.return_value = expected_s3_url

        #  Artifact path is a Directory
        with self.make_temp_dir() as artifact_path:
            # Artifact is a file in the temporary directory
            parent_dir = tempfile.gettempdir()
            resource_dict = {property_name: artifact_path}

            result = upload_local_artifacts(
                resource_id,
                resource_dict,
                property_name,
                parent_dir,
                mock.Mock(),
            )
            self.assertEqual(result, expected_s3_url)

            absolute_artifact_path = make_abs_path(parent_dir, artifact_path)

            zip_and_upload_mock.assert_called_once_with(
                absolute_artifact_path, mock.ANY
            )

    @mock.patch(
        "awscli.customizations.cloudformation.artifact_exporter.zip_and_upload"
    )
    def test_upload_local_artifacts_no_path(self, zip_and_upload_mock):
        property_name = "property"
        resource_id = "resource_id"
        expected_s3_url = "s3://foo/bar?versionId=baz"

        zip_and_upload_mock.return_value = expected_s3_url

        # If you don't specify a path, we will default to Current Working Dir
        resource_dict = {}
        parent_dir = tempfile.gettempdir()

        result = upload_local_artifacts(
            resource_id,
            resource_dict,
            property_name,
            parent_dir,
            self.s3_uploader_mock,
        )
        self.assertEqual(result, expected_s3_url)

        zip_and_upload_mock.assert_called_once_with(parent_dir, mock.ANY)
        self.s3_uploader_mock.upload_with_dedup.assert_not_called()

    @mock.patch(
        "awscli.customizations.cloudformation.artifact_exporter.zip_and_upload"
    )
    def test_upload_local_artifacts_s3_url(self, zip_and_upload_mock):
        property_name = "property"
        resource_id = "resource_id"
        object_s3_url = "s3://foo/bar?versionId=baz"

        # If URL is already S3 URL, this will be returned without zip/upload
        resource_dict = {property_name: object_s3_url}
        parent_dir = tempfile.gettempdir()

        result = upload_local_artifacts(
            resource_id,
            resource_dict,
            property_name,
            parent_dir,
            self.s3_uploader_mock,
        )
        self.assertEqual(result, object_s3_url)

        zip_and_upload_mock.assert_not_called()
        self.s3_uploader_mock.upload_with_dedup.assert_not_called()

    @mock.patch(
        "awscli.customizations.cloudformation.artifact_exporter.zip_and_upload"
    )
    def test_upload_local_artifacts_invalid_value(self, zip_and_upload_mock):
        property_name = "property"
        resource_id = "resource_id"
        parent_dir = tempfile.gettempdir()

        with self.assertRaises(exceptions.InvalidLocalPathError):
            non_existent_file = "some_random_filename"
            resource_dict = {property_name: non_existent_file}
            upload_local_artifacts(
                resource_id,
                resource_dict,
                property_name,
                parent_dir,
                self.s3_uploader_mock,
            )

        with self.assertRaises(exceptions.InvalidLocalPathError):
            non_existent_file = ["invalid datatype"]
            resource_dict = {property_name: non_existent_file}
            upload_local_artifacts(
                resource_id,
                resource_dict,
                property_name,
                parent_dir,
                self.s3_uploader_mock,
            )

        zip_and_upload_mock.assert_not_called()
        self.s3_uploader_mock.upload_with_dedup.assert_not_called()

    @mock.patch(
        "awscli.customizations.cloudformation.artifact_exporter.make_zip"
    )
    def test_zip_folder(self, make_zip_mock):
        zip_file_name = "name.zip"
        make_zip_mock.return_value = zip_file_name

        with self.make_temp_dir() as dirname:
            with zip_folder(dirname) as actual_zip_file_name:
                self.assertEqual(actual_zip_file_name, zip_file_name)

        make_zip_mock.assert_called_once_with(mock.ANY, dirname)

    @mock.patch(
        "awscli.customizations.cloudformation.artifact_exporter.upload_local_artifacts"
    )
    def test_resource(self, upload_local_artifacts_mock):
        # Property value is a path to file

        class MockResource(Resource):
            PROPERTY_NAME = "foo"

        resource = MockResource(self.s3_uploader_mock)

        resource_id = "id"
        resource_dict = {}
        resource_dict[resource.PROPERTY_NAME] = "/path/to/file"
        parent_dir = "dir"
        s3_url = "s3://foo/bar"

        upload_local_artifacts_mock.return_value = s3_url

        resource.export(resource_id, resource_dict, parent_dir)

        upload_local_artifacts_mock.assert_called_once_with(
            resource_id,
            resource_dict,
            resource.PROPERTY_NAME,
            parent_dir,
            self.s3_uploader_mock,
        )

        self.assertEqual(resource_dict[resource.PROPERTY_NAME], s3_url)

    @mock.patch("shutil.rmtree")
    @mock.patch("zipfile.is_zipfile")
    @mock.patch(
        "awscli.customizations.cloudformation.artifact_exporter.copy_to_temp_dir"
    )
    @mock.patch(
        "awscli.customizations.cloudformation.artifact_exporter.zip_and_upload"
    )
    @mock.patch(
        "awscli.customizations.cloudformation.artifact_exporter.is_local_file"
    )
    def test_resource_with_force_zip_on_regular_file(
        self,
        is_local_file_mock,
        zip_and_upload_mock,
        copy_to_temp_dir_mock,
        is_zipfile_mock,
        rmtree_mock,
    ):
        # Property value is a path to file and FORCE_ZIP is True

        class MockResource(Resource):
            PROPERTY_NAME = "foo"
            FORCE_ZIP = True

        resource = MockResource(self.s3_uploader_mock)

        resource_id = "id"
        resource_dict = {}
        original_path = "/path/to/file"
        resource_dict[resource.PROPERTY_NAME] = original_path
        parent_dir = "dir"
        s3_url = "s3://foo/bar"

        zip_and_upload_mock.return_value = s3_url
        is_local_file_mock.return_value = True

        with self.make_temp_dir() as tmp_dir:
            copy_to_temp_dir_mock.return_value = tmp_dir

            # This is not a zip file
            is_zipfile_mock.return_value = False

            resource.export(resource_id, resource_dict, parent_dir)

            zip_and_upload_mock.assert_called_once_with(tmp_dir, mock.ANY)
            rmtree_mock.assert_called_once_with(tmp_dir)
            is_zipfile_mock.assert_called_once_with(original_path)
            assert resource_dict[resource.PROPERTY_NAME] == s3_url

    @mock.patch("shutil.rmtree")
    @mock.patch("zipfile.is_zipfile")
    @mock.patch(
        "awscli.customizations.cloudformation.artifact_exporter.copy_to_temp_dir"
    )
    @mock.patch(
        "awscli.customizations.cloudformation.artifact_exporter.zip_and_upload"
    )
    @mock.patch(
        "awscli.customizations.cloudformation.artifact_exporter.is_local_file"
    )
    def test_resource_with_force_zip_on_zip_file(
        self,
        is_local_file_mock,
        zip_and_upload_mock,
        copy_to_temp_dir_mock,
        is_zipfile_mock,
        rmtree_mock,
    ):
        # Property value is a path to zip file and FORCE_ZIP is True
        # We should *not* re-zip an existing zip

        class MockResource(Resource):
            PROPERTY_NAME = "foo"
            FORCE_ZIP = True

        resource = MockResource(self.s3_uploader_mock)

        resource_id = "id"
        resource_dict = {}
        original_path = "/path/to/zip_file"
        resource_dict[resource.PROPERTY_NAME] = original_path
        parent_dir = "dir"
        s3_url = "s3://foo/bar"

        # When the file is actually a zip-file, no additional zipping has to happen
        is_zipfile_mock.return_value = True
        is_local_file_mock.return_value = True
        zip_and_upload_mock.return_value = s3_url
        self.s3_uploader_mock.upload_with_dedup.return_value = s3_url

        resource.export(resource_id, resource_dict, parent_dir)

        copy_to_temp_dir_mock.assert_not_called()
        zip_and_upload_mock.assert_not_called()
        rmtree_mock.assert_not_called()
        is_zipfile_mock.assert_called_once_with(original_path)
        assert resource_dict[resource.PROPERTY_NAME] == s3_url

    @mock.patch("shutil.rmtree")
    @mock.patch("zipfile.is_zipfile")
    @mock.patch(
        "awscli.customizations.cloudformation.artifact_exporter.copy_to_temp_dir"
    )
    @mock.patch(
        "awscli.customizations.cloudformation.artifact_exporter.zip_and_upload"
    )
    @mock.patch(
        "awscli.customizations.cloudformation.artifact_exporter.is_local_file"
    )
    def test_resource_without_force_zip(
        self,
        is_local_file_mock,
        zip_and_upload_mock,
        copy_to_temp_dir_mock,
        is_zipfile_mock,
        rmtree_mock,
    ):
        class MockResourceNoForceZip(Resource):
            PROPERTY_NAME = "foo"

        resource = MockResourceNoForceZip(self.s3_uploader_mock)

        resource_id = "id"
        resource_dict = {}
        original_path = "/path/to/file"
        resource_dict[resource.PROPERTY_NAME] = original_path
        parent_dir = "dir"
        s3_url = "s3://foo/bar"

        # This is not a zip file, but a valid local file. Since FORCE_ZIP is NOT set, this will not be zipped
        is_zipfile_mock.return_value = False
        is_local_file_mock.return_value = True
        zip_and_upload_mock.return_value = s3_url
        self.s3_uploader_mock.upload_with_dedup.return_value = s3_url

        resource.export(resource_id, resource_dict, parent_dir)

        copy_to_temp_dir_mock.assert_not_called()
        zip_and_upload_mock.assert_not_called()
        rmtree_mock.assert_not_called()
        is_zipfile_mock.assert_called_once_with(original_path)
        assert resource_dict[resource.PROPERTY_NAME] == s3_url

    @mock.patch(
        "awscli.customizations.cloudformation.artifact_exporter.upload_local_artifacts"
    )
    def test_resource_empty_property_value(self, upload_local_artifacts_mock):
        # Property value is empty

        class MockResource(Resource):
            PROPERTY_NAME = "foo"

        resource = MockResource(self.s3_uploader_mock)

        resource_id = "id"
        resource_dict = {}
        resource_dict[resource.PROPERTY_NAME] = "/path/to/file"
        parent_dir = "dir"
        s3_url = "s3://foo/bar"

        upload_local_artifacts_mock.return_value = s3_url
        resource_dict = {}
        resource.export(resource_id, resource_dict, parent_dir)
        upload_local_artifacts_mock.assert_called_once_with(
            resource_id,
            resource_dict,
            resource.PROPERTY_NAME,
            parent_dir,
            self.s3_uploader_mock,
        )
        self.assertEqual(resource_dict[resource.PROPERTY_NAME], s3_url)

    @mock.patch(
        "awscli.customizations.cloudformation.artifact_exporter.upload_local_artifacts"
    )
    def test_resource_property_value_dict(self, upload_local_artifacts_mock):
        # Property value is a dictionary. Export should not upload anything

        class MockResource(Resource):
            PROPERTY_NAME = "foo"

        resource = MockResource(self.s3_uploader_mock)
        resource_id = "id"
        resource_dict = {}
        resource_dict[resource.PROPERTY_NAME] = "/path/to/file"
        parent_dir = "dir"
        s3_url = "s3://foo/bar"

        upload_local_artifacts_mock.return_value = s3_url
        resource_dict = {}
        resource_dict[resource.PROPERTY_NAME] = {"a": "b"}
        resource.export(resource_id, resource_dict, parent_dir)
        upload_local_artifacts_mock.assert_not_called()
        self.assertEqual(resource_dict, {"foo": {"a": "b"}})

    @mock.patch(
        "awscli.customizations.cloudformation.artifact_exporter.upload_local_artifacts"
    )
    def test_resource_has_package_null_property_to_false(
        self, upload_local_artifacts_mock
    ):
        # Should not upload anything if PACKAGE_NULL_PROPERTY is set to False

        class MockResource(Resource):
            PROPERTY_NAME = "foo"
            PACKAGE_NULL_PROPERTY = False

        resource = MockResource(self.s3_uploader_mock)
        resource_id = "id"
        resource_dict = {}
        parent_dir = "dir"
        s3_url = "s3://foo/bar"

        upload_local_artifacts_mock.return_value = s3_url

        resource.export(resource_id, resource_dict, parent_dir)

        upload_local_artifacts_mock.assert_not_called()
        self.assertNotIn(resource.PROPERTY_NAME, resource_dict)

    @mock.patch(
        "awscli.customizations.cloudformation.artifact_exporter.upload_local_artifacts"
    )
    def test_resource_export_fails(self, upload_local_artifacts_mock):
        class MockResource(Resource):
            PROPERTY_NAME = "foo"

        resource = MockResource(self.s3_uploader_mock)
        resource_id = "id"
        resource_dict = {}
        resource_dict[resource.PROPERTY_NAME] = "/path/to/file"
        parent_dir = "dir"
        s3_url = "s3://foo/bar"

        upload_local_artifacts_mock.side_effect = RuntimeError
        resource_dict = {}

        with self.assertRaises(exceptions.ExportFailedError):
            resource.export(resource_id, resource_dict, parent_dir)

    @mock.patch(
        "awscli.customizations.cloudformation.artifact_exporter.upload_local_artifacts"
    )
    def test_resource_with_s3_url_dict(self, upload_local_artifacts_mock):
        """
        Checks if we properly export from the Resource classc
        :return:
        """

        self.assertTrue(issubclass(ResourceWithS3UrlDict, Resource))

        class MockResource(ResourceWithS3UrlDict):
            PROPERTY_NAME = "foo"
            BUCKET_NAME_PROPERTY = "b"
            OBJECT_KEY_PROPERTY = "o"
            VERSION_PROPERTY = "v"

        resource = MockResource(self.s3_uploader_mock)

        # Case 1: Property value is a path to file
        resource_id = "id"
        resource_dict = {}
        resource_dict[resource.PROPERTY_NAME] = "/path/to/file"
        parent_dir = "dir"
        s3_url = "s3://bucket/key1/key2?versionId=SomeVersionNumber"

        upload_local_artifacts_mock.return_value = s3_url

        resource.export(resource_id, resource_dict, parent_dir)

        upload_local_artifacts_mock.assert_called_once_with(
            resource_id,
            resource_dict,
            resource.PROPERTY_NAME,
            parent_dir,
            self.s3_uploader_mock,
        )

        self.assertEqual(
            resource_dict[resource.PROPERTY_NAME],
            {"b": "bucket", "o": "key1/key2", "v": "SomeVersionNumber"},
        )

    @mock.patch(
        "awscli.customizations.cloudformation.artifact_exporter.Template"
    )
    def test_export_cloudformation_stack(self, TemplateMock):
        stack_resource = CloudFormationStackResource(self.s3_uploader_mock)

        resource_id = "id"
        property_name = stack_resource.PROPERTY_NAME
        exported_template_dict = {"foo": "bar"}
        result_s3_url = "s3://hello/world"
        result_path_style_s3_url = "http://s3.amazonws.com/hello/world"

        template_instance_mock = mock.Mock()
        TemplateMock.return_value = template_instance_mock
        template_instance_mock.export.return_value = exported_template_dict

        self.s3_uploader_mock.upload_with_dedup.return_value = result_s3_url
        self.s3_uploader_mock.to_path_style_s3_url.return_value = (
            result_path_style_s3_url
        )

        with tempfile.NamedTemporaryFile() as handle:
            template_path = handle.name
            resource_dict = {property_name: template_path}
            parent_dir = tempfile.gettempdir()

            stack_resource.export(resource_id, resource_dict, parent_dir)

            self.assertEqual(
                resource_dict[property_name], result_path_style_s3_url
            )

            TemplateMock.assert_called_once_with(
                template_path, parent_dir, self.s3_uploader_mock
            )
            template_instance_mock.export.assert_called_once_with()
            self.s3_uploader_mock.upload_with_dedup.assert_called_once_with(
                mock.ANY, "template"
            )
            self.s3_uploader_mock.to_path_style_s3_url.assert_called_once_with(
                "world", None
            )

    def _assert_stack_template_url_is_not_changed(self, s3_url):
        stack_resource = CloudFormationStackResource(self.s3_uploader_mock)
        resource_id = "id"
        property_name = stack_resource.PROPERTY_NAME
        resource_dict = {property_name: s3_url}

        # Case 1: Path is already S3 url
        stack_resource.export(resource_id, resource_dict, "dir")
        self.assertEqual(resource_dict[property_name], s3_url)
        self.s3_uploader_mock.upload_with_dedup.assert_not_called()

    def test_export_cloudformation_stack_no_upload_path_is_s3url(self):
        s3_url = "s3://hello/world"
        self._assert_stack_template_url_is_not_changed(s3_url)

    def test_export_cloudformation_stack_no_upload_path_is_httpsurl(self):
        s3_url = "https://s3.amazonaws.com/hello/world"
        self._assert_stack_template_url_is_not_changed(s3_url)

    def test_export_cloudformation_stack_no_upload_path_is_s3_region_httpsurl(
        self,
    ):
        s3_url = "https://s3.some-valid-region.amazonaws.com/hello/world"
        self._assert_stack_template_url_is_not_changed(s3_url)

    def test_export_cloudformation_stack_no_upload_path_is_virtual(self):
        s3_url = "https://bucket.s3.amazonaws.com/key"
        self._assert_stack_template_url_is_not_changed(s3_url)

    def test_export_cloudformation_stack_no_upload_path_is_virtual_region(
        self,
    ):
        s3_url = "https://bucket.s3.some-region.amazonaws.com/key"
        self._assert_stack_template_url_is_not_changed(s3_url)

    def test_export_cloudformation_stack_no_upload_path_is_empty(self):
        stack_resource = CloudFormationStackResource(self.s3_uploader_mock)
        resource_id = "id"
        property_name = stack_resource.PROPERTY_NAME
        s3_url = "s3://hello/world"
        resource_dict = {property_name: s3_url}

        # Case 2: Path is empty
        resource_dict = {}
        stack_resource.export(resource_id, resource_dict, "dir")
        self.assertEqual(resource_dict, {})
        self.s3_uploader_mock.upload_with_dedup.assert_not_called()

    def test_export_cloudformation_stack_no_upload_path_not_file(self):
        stack_resource = CloudFormationStackResource(self.s3_uploader_mock)
        resource_id = "id"
        property_name = stack_resource.PROPERTY_NAME
        s3_url = "s3://hello/world"

        # Case 3: Path is not a file
        with self.make_temp_dir() as dirname:
            resource_dict = {property_name: dirname}
            with self.assertRaises(exceptions.ExportFailedError):
                stack_resource.export(resource_id, resource_dict, "dir")
                self.s3_uploader_mock.upload_with_dedup.assert_not_called()

    @mock.patch(
        "awscli.customizations.cloudformation.artifact_exporter.Template"
    )
    def test_export_serverless_application(self, TemplateMock):
        stack_resource = ServerlessApplicationResource(self.s3_uploader_mock)

        resource_id = "id"
        property_name = stack_resource.PROPERTY_NAME
        exported_template_dict = {"foo": "bar"}
        result_s3_url = "s3://hello/world"
        result_path_style_s3_url = "http://s3.amazonws.com/hello/world"

        template_instance_mock = mock.Mock()
        TemplateMock.return_value = template_instance_mock
        template_instance_mock.export.return_value = exported_template_dict

        self.s3_uploader_mock.upload_with_dedup.return_value = result_s3_url
        self.s3_uploader_mock.to_path_style_s3_url.return_value = (
            result_path_style_s3_url
        )

        with tempfile.NamedTemporaryFile() as handle:
            template_path = handle.name
            resource_dict = {property_name: template_path}
            parent_dir = tempfile.gettempdir()

            stack_resource.export(resource_id, resource_dict, parent_dir)

            self.assertEqual(
                resource_dict[property_name], result_path_style_s3_url
            )

            TemplateMock.assert_called_once_with(
                template_path, parent_dir, self.s3_uploader_mock
            )
            template_instance_mock.export.assert_called_once_with()
            self.s3_uploader_mock.upload_with_dedup.assert_called_once_with(
                mock.ANY, "template"
            )
            self.s3_uploader_mock.to_path_style_s3_url.assert_called_once_with(
                "world", None
            )

    def test_export_serverless_application_no_upload_path_is_s3url(self):
        stack_resource = ServerlessApplicationResource(self.s3_uploader_mock)
        resource_id = "id"
        property_name = stack_resource.PROPERTY_NAME
        s3_url = "s3://hello/world"
        resource_dict = {property_name: s3_url}

        # Case 1: Path is already S3 url
        stack_resource.export(resource_id, resource_dict, "dir")
        self.assertEqual(resource_dict[property_name], s3_url)
        self.s3_uploader_mock.upload_with_dedup.assert_not_called()

    def test_export_serverless_application_no_upload_path_is_httpsurl(self):
        stack_resource = ServerlessApplicationResource(self.s3_uploader_mock)
        resource_id = "id"
        property_name = stack_resource.PROPERTY_NAME
        s3_url = "https://s3.amazonaws.com/hello/world"
        resource_dict = {property_name: s3_url}

        # Case 1: Path is already S3 url
        stack_resource.export(resource_id, resource_dict, "dir")
        self.assertEqual(resource_dict[property_name], s3_url)
        self.s3_uploader_mock.upload_with_dedup.assert_not_called()

    def test_export_serverless_application_no_upload_path_is_empty(self):
        stack_resource = ServerlessApplicationResource(self.s3_uploader_mock)
        resource_id = "id"
        property_name = stack_resource.PROPERTY_NAME

        # Case 2: Path is empty
        resource_dict = {}
        stack_resource.export(resource_id, resource_dict, "dir")
        self.assertEqual(resource_dict, {})
        self.s3_uploader_mock.upload_with_dedup.assert_not_called()

    def test_export_serverless_application_no_upload_path_not_file(self):
        stack_resource = ServerlessApplicationResource(self.s3_uploader_mock)
        resource_id = "id"
        property_name = stack_resource.PROPERTY_NAME

        # Case 3: Path is not a file
        with self.make_temp_dir() as dirname:
            resource_dict = {property_name: dirname}
            with self.assertRaises(exceptions.ExportFailedError):
                stack_resource.export(resource_id, resource_dict, "dir")
                self.s3_uploader_mock.upload_with_dedup.assert_not_called()

    def test_export_serverless_application_no_upload_path_is_dictionary(self):
        stack_resource = ServerlessApplicationResource(self.s3_uploader_mock)
        resource_id = "id"
        property_name = stack_resource.PROPERTY_NAME

        # Case 4: Path is dictionary
        location = {"ApplicationId": "id", "SemanticVersion": "1.0.1"}
        resource_dict = {property_name: location}
        stack_resource.export(resource_id, resource_dict, "dir")
        self.assertEqual(resource_dict[property_name], location)
        self.s3_uploader_mock.upload_with_dedup.assert_not_called()

    @mock.patch(
        "awscli.customizations.cloudformation.artifact_exporter.yaml_parse"
    )
    def test_template_export_metadata(self, yaml_parse_mock):
        parent_dir = os.path.abspath(os.path.sep)
        template_dir = os.path.join(parent_dir, 'foo', 'bar')
        template_path = os.path.join(template_dir, 'path')
        template_str = self.example_yaml_template()

        metadata_type1_class = mock.Mock()
        metadata_type1_class.RESOURCE_TYPE = "metadata_type1"
        metadata_type1_class.PROPERTY_NAME = "property_1"
        metadata_type1_instance = mock.Mock()
        metadata_type1_class.return_value = metadata_type1_instance

        metadata_type2_class = mock.Mock()
        metadata_type2_class.RESOURCE_TYPE = "metadata_type2"
        metadata_type2_class.PROPERTY_NAME = "property_2"
        metadata_type2_instance = mock.Mock()
        metadata_type2_class.return_value = metadata_type2_instance

        metadata_to_export = [metadata_type1_class, metadata_type2_class]

        template_dict = {
            "Metadata": {
                "metadata_type1": {"property_1": "abc"},
                "metadata_type2": {"property_2": "def"},
            }
        }
        open_mock = mock.mock_open()
        yaml_parse_mock.return_value = template_dict

        # Patch the file open method to return template string
        with mock.patch(
            "awscli.customizations.cloudformation."
            "artifact_exporter.compat_open",
            open_mock(read_data=template_str),
        ) as open_mock:
            template_exporter = Template(
                template_path,
                parent_dir,
                self.s3_uploader_mock,
                metadata_to_export=metadata_to_export,
            )
            exported_template = template_exporter.export()
            self.assertEqual(exported_template, template_dict)

            open_mock.assert_called_once_with(
                make_abs_path(parent_dir, template_path), "r"
            )

            self.assertEqual(1, yaml_parse_mock.call_count)

            metadata_type1_class.assert_called_once_with(self.s3_uploader_mock)
            metadata_type1_instance.export.assert_called_once_with(
                "metadata_type1", mock.ANY, template_dir
            )
            metadata_type2_class.assert_called_once_with(self.s3_uploader_mock)
            metadata_type2_instance.export.assert_called_once_with(
                "metadata_type2", mock.ANY, template_dir
            )

    @mock.patch(
        "awscli.customizations.cloudformation.artifact_exporter.yaml_parse"
    )
    def test_template_export(self, yaml_parse_mock):
        parent_dir = os.path.abspath(os.path.sep)
        template_dir = os.path.join(parent_dir, 'foo', 'bar')
        template_path = os.path.join(template_dir, 'path')
        template_str = self.example_yaml_template()

        resource_type1_class = mock.Mock()
        resource_type1_class.RESOURCE_TYPE = "resource_type1"
        resource_type1_instance = mock.Mock()
        resource_type1_class.return_value = resource_type1_instance
        resource_type2_class = mock.Mock()
        resource_type2_class.RESOURCE_TYPE = "resource_type2"
        resource_type2_instance = mock.Mock()
        resource_type2_class.return_value = resource_type2_instance

        resources_to_export = [resource_type1_class, resource_type2_class]

        properties = {"foo": "bar"}
        template_dict = {
            "Resources": {
                "Resource1": {
                    "Type": "resource_type1",
                    "Properties": properties,
                },
                "Resource2": {
                    "Type": "resource_type2",
                    "Properties": properties,
                },
                "Resource3": {
                    "Type": "some-other-type",
                    "Properties": properties,
                },
            }
        }

        open_mock = mock.mock_open()
        yaml_parse_mock.return_value = template_dict

        # Patch the file open method to return template string
        with mock.patch(
            "awscli.customizations.cloudformation."
            "artifact_exporter.compat_open",
            open_mock(read_data=template_str),
        ) as open_mock:
            template_exporter = Template(
                template_path,
                parent_dir,
                self.s3_uploader_mock,
                resources_to_export,
            )
            exported_template = template_exporter.export()
            self.assertEqual(exported_template, template_dict)

            open_mock.assert_called_once_with(
                make_abs_path(parent_dir, template_path), "r"
            )

            self.assertEqual(1, yaml_parse_mock.call_count)

            resource_type1_class.assert_called_once_with(self.s3_uploader_mock)
            resource_type1_instance.export.assert_called_once_with(
                "Resource1", mock.ANY, template_dir
            )
            resource_type2_class.assert_called_once_with(self.s3_uploader_mock)
            resource_type2_instance.export.assert_called_once_with(
                "Resource2", mock.ANY, template_dir
            )

    @mock.patch(
        "awscli.customizations.cloudformation.artifact_exporter.yaml_parse"
    )
    def test_template_export_foreach_valid(self, yaml_parse_mock):
        parent_dir = os.path.abspath(os.path.sep)
        template_dir = os.path.join(parent_dir, 'foo', 'bar')
        template_path = os.path.join(template_dir, 'path')
        template_str = self.example_yaml_template()

        resource_type1_class = mock.Mock()
        resource_type1_class.RESOURCE_TYPE = "resource_type1"
        resource_type1_instance = mock.Mock()
        resource_type1_class.return_value = resource_type1_instance
        resource_type2_class = mock.Mock()
        resource_type2_class.RESOURCE_TYPE = "resource_type2"
        resource_type2_instance = mock.Mock()
        resource_type2_class.return_value = resource_type2_instance

        resources_to_export = [resource_type1_class, resource_type2_class]

        properties = {"foo": "bar"}
        template_dict = {
            "Resources": {
                "Resource1": {
                    "Type": "resource_type1",
                    "Properties": properties,
                },
                "Resource2": {
                    "Type": "resource_type2",
                    "Properties": properties,
                },
                "Resource3": {
                    "Type": "some-other-type",
                    "Properties": properties,
                },
                "Fn::ForEach::OuterLoopName": [
                    "Identifier1",
                    ["4", "5"],
                    {
                        "Fn::ForEach::InnerLoopName": [
                            "Identifier2",
                            ["6", "7"],
                            {
                                "Resource${Identifier1}${Identifier2}": {
                                    "Type": "resource_type2",
                                    "Properties": properties,
                                }
                            },
                        ],
                        "Resource${Identifier1}": {
                            "Type": "resource_type1",
                            "Properties": properties,
                        },
                    },
                ],
            }
        }

        open_mock = mock.mock_open()
        yaml_parse_mock.return_value = template_dict

        # Patch the file open method to return template string
        with mock.patch(
            "awscli.customizations.cloudformation."
            "artifact_exporter.compat_open",
            open_mock(read_data=template_str),
        ) as open_mock:
            template_exporter = Template(
                template_path,
                parent_dir,
                self.s3_uploader_mock,
                resources_to_export,
            )
            exported_template = template_exporter.export()
            self.assertEqual(exported_template, template_dict)

            open_mock.assert_called_once_with(
                make_abs_path(parent_dir, template_path), "r"
            )

            self.assertEqual(1, yaml_parse_mock.call_count)

            resource_type1_class.assert_called_with(self.s3_uploader_mock)
            self.assertEqual(
                resource_type1_instance.export.call_args_list,
                [
                    mock.call("Resource1", properties, template_dir),
                    mock.call(
                        "Resource${Identifier1}", properties, template_dir
                    ),
                ],
            )
            resource_type2_class.assert_called_with(self.s3_uploader_mock)
            self.assertEqual(
                resource_type2_instance.export.call_args_list,
                [
                    mock.call("Resource2", properties, template_dir),
                    mock.call(
                        "Resource${Identifier1}${Identifier2}",
                        properties,
                        template_dir,
                    ),
                ],
            )

    @mock.patch(
        "awscli.customizations.cloudformation.artifact_exporter.yaml_parse"
    )
    def test_template_export_foreach_invalid(self, yaml_parse_mock):
        parent_dir = os.path.abspath(os.path.sep)
        template_dir = os.path.join(parent_dir, 'foo', 'bar')
        template_path = os.path.join(template_dir, 'path')
        template_str = self.example_yaml_template()

        resource_type1_class = mock.Mock()
        resource_type1_class.RESOURCE_TYPE = "resource_type1"
        resource_type1_instance = mock.Mock()
        resource_type1_class.return_value = resource_type1_instance
        resource_type2_class = mock.Mock()
        resource_type2_class.RESOURCE_TYPE = "resource_type2"
        resource_type2_instance = mock.Mock()
        resource_type2_class.return_value = resource_type2_instance

        resources_to_export = [resource_type1_class, resource_type2_class]

        properties = {"foo": "bar"}
        template_dict = {
            "Resources": {
                "Resource1": {
                    "Type": "resource_type1",
                    "Properties": properties,
                },
                "Resource2": {
                    "Type": "resource_type2",
                    "Properties": properties,
                },
                "Resource3": {
                    "Type": "some-other-type",
                    "Properties": properties,
                },
                "Fn::ForEach::OuterLoopName": [
                    "Identifier1",
                    {"Resource${Identifier1}": {}},
                ],
            }
        }

        open_mock = mock.mock_open()
        yaml_parse_mock.return_value = template_dict

        # Patch the file open method to return template string
        with mock.patch(
            "awscli.customizations.cloudformation."
            "artifact_exporter.compat_open",
            open_mock(read_data=template_str),
        ) as open_mock:
            template_exporter = Template(
                template_path,
                parent_dir,
                self.s3_uploader_mock,
                resources_to_export,
            )
            with self.assertRaises(
                exceptions.InvalidForEachIntrinsicFunctionError
            ):
                template_exporter.export()

    @mock.patch(
        "awscli.customizations.cloudformation.artifact_exporter.yaml_parse"
    )
    def test_template_global_export(self, yaml_parse_mock):
        parent_dir = os.path.abspath(os.path.sep)
        template_dir = os.path.join(parent_dir, 'foo', 'bar')
        template_path = os.path.join(template_dir, 'path')
        template_str = self.example_yaml_template()

        resource_type1_class = mock.Mock()
        resource_type1_instance = mock.Mock()
        resource_type1_class.return_value = resource_type1_instance
        resource_type2_class = mock.Mock()
        resource_type2_instance = mock.Mock()
        resource_type2_class.return_value = resource_type2_instance

        resources_to_export = {
            "resource_type1": resource_type1_class,
            "resource_type2": resource_type2_class,
        }
        properties1 = {
            "foo": "bar",
            "Fn::Transform": {
                "Name": "AWS::Include",
                "Parameters": {"Location": "foo.yaml"},
            },
        }
        properties2 = {
            "foo": "bar",
            "Fn::Transform": {"Name": "AWS::OtherTransform"},
        }
        properties_in_list = {
            "Fn::Transform": {
                "Name": "AWS::Include",
                "Parameters": {"Location": "bar.yaml"},
            }
        }
        template_dict = {
            "Resources": {
                "Resource1": {
                    "Type": "resource_type1",
                    "Properties": properties1,
                },
                "Resource2": {
                    "Type": "resource_type2",
                    "Properties": properties2,
                },
            },
            "List": ["foo", properties_in_list],
        }
        open_mock = mock.mock_open()
        include_transform_export_handler_mock = mock.Mock()
        include_transform_export_handler_mock.return_value = {
            "Name": "AWS::Include",
            "Parameters": {"Location": "s3://foo"},
        }
        yaml_parse_mock.return_value = template_dict

        with mock.patch(
            "awscli.customizations.cloudformation."
            "artifact_exporter.compat_open",
            open_mock(read_data=template_str),
        ) as open_mock:
            with mock.patch.dict(
                GLOBAL_EXPORT_DICT,
                {"Fn::Transform": include_transform_export_handler_mock},
            ):
                template_exporter = Template(
                    template_path,
                    parent_dir,
                    self.s3_uploader_mock,
                    resources_to_export,
                )

                exported_template = template_exporter.export_global_artifacts(
                    template_exporter.template_dict
                )

                first_call_args, kwargs = (
                    include_transform_export_handler_mock.call_args_list[0]
                )
                second_call_args, kwargs = (
                    include_transform_export_handler_mock.call_args_list[1]
                )
                third_call_args, kwargs = (
                    include_transform_export_handler_mock.call_args_list[2]
                )
                call_args = [
                    first_call_args[0],
                    second_call_args[0],
                    third_call_args[0],
                ]
                self.assertTrue(
                    {
                        "Name": "AWS::Include",
                        "Parameters": {"Location": "foo.yaml"},
                    }
                    in call_args
                )
                self.assertTrue({"Name": "AWS::OtherTransform"} in call_args)
                self.assertTrue(
                    {
                        "Name": "AWS::Include",
                        "Parameters": {"Location": "bar.yaml"},
                    }
                    in call_args
                )
                self.assertTrue(template_dir in first_call_args)
                self.assertTrue(template_dir in second_call_args)
                self.assertTrue(template_dir in third_call_args)
                self.assertEqual(
                    include_transform_export_handler_mock.call_count, 3
                )
                # new s3 url is added to include location
                self.assertEqual(
                    exported_template["Resources"]["Resource1"]["Properties"][
                        "Fn::Transform"
                    ],
                    {
                        "Name": "AWS::Include",
                        "Parameters": {"Location": "s3://foo"},
                    },
                )
                self.assertEqual(
                    exported_template["List"][1]["Fn::Transform"],
                    {
                        "Name": "AWS::Include",
                        "Parameters": {"Location": "s3://foo"},
                    },
                )

    @mock.patch(
        "awscli.customizations.cloudformation.artifact_exporter.is_local_file"
    )
    def test_include_transform_export_handler_with_relative_file_path(
        self, is_local_file_mock
    ):
        # exports transform
        parent_dir = os.path.abspath("someroot")
        self.s3_uploader_mock.upload_with_dedup.return_value = "s3://foo"
        is_local_file_mock.return_value = True
        abs_file_path = os.path.join(parent_dir, "foo.yaml")

        handler_output = include_transform_export_handler(
            {"Name": "AWS::Include", "Parameters": {"Location": "foo.yaml"}},
            self.s3_uploader_mock,
            parent_dir,
        )
        self.s3_uploader_mock.upload_with_dedup.assert_called_once_with(
            abs_file_path
        )
        is_local_file_mock.assert_called_with(abs_file_path)
        self.assertEqual(
            handler_output,
            {'Name': 'AWS::Include', 'Parameters': {'Location': 's3://foo'}},
        )

    @mock.patch(
        "awscli.customizations.cloudformation.artifact_exporter.is_local_file"
    )
    def test_include_transform_export_handler_with_absolute_file_path(
        self, is_local_file_mock
    ):
        # exports transform
        parent_dir = os.path.abspath("someroot")
        self.s3_uploader_mock.upload_with_dedup.return_value = "s3://foo"
        is_local_file_mock.return_value = True
        abs_file_path = os.path.abspath(os.path.join("my", "file.yaml"))

        handler_output = include_transform_export_handler(
            {
                "Name": "AWS::Include",
                "Parameters": {"Location": abs_file_path},
            },
            self.s3_uploader_mock,
            parent_dir,
        )
        self.s3_uploader_mock.upload_with_dedup.assert_called_once_with(
            abs_file_path
        )
        is_local_file_mock.assert_called_with(abs_file_path)
        self.assertEqual(
            handler_output,
            {'Name': 'AWS::Include', 'Parameters': {'Location': 's3://foo'}},
        )

    @mock.patch(
        "awscli.customizations.cloudformation.artifact_exporter.is_local_file"
    )
    def test_include_transform_export_handler_with_s3_uri(
        self, is_local_file_mock
    ):
        handler_output = include_transform_export_handler(
            {
                "Name": "AWS::Include",
                "Parameters": {"Location": "s3://bucket/foo.yaml"},
            },
            self.s3_uploader_mock,
            "parent_dir",
        )
        # Input is returned unmodified
        self.assertEqual(
            handler_output,
            {
                "Name": "AWS::Include",
                "Parameters": {"Location": "s3://bucket/foo.yaml"},
            },
        )

        is_local_file_mock.assert_not_called()
        self.s3_uploader_mock.assert_not_called()

    @mock.patch(
        "awscli.customizations.cloudformation.artifact_exporter.is_local_file"
    )
    def test_include_transform_export_handler_with_no_path(
        self, is_local_file_mock
    ):
        handler_output = include_transform_export_handler(
            {"Name": "AWS::Include", "Parameters": {"Location": ""}},
            self.s3_uploader_mock,
            "parent_dir",
        )
        # Input is returned unmodified
        self.assertEqual(
            handler_output,
            {"Name": "AWS::Include", "Parameters": {"Location": ""}},
        )

        is_local_file_mock.assert_not_called()
        self.s3_uploader_mock.assert_not_called()

    @mock.patch(
        "awscli.customizations.cloudformation.artifact_exporter.is_local_file"
    )
    def test_include_transform_export_handler_with_dict_value_for_location(
        self, is_local_file_mock
    ):
        handler_output = include_transform_export_handler(
            {
                "Name": "AWS::Include",
                "Parameters": {
                    "Location": {"Fn::Sub": "${S3Bucket}/file.txt"}
                },
            },
            self.s3_uploader_mock,
            "parent_dir",
        )
        # Input is returned unmodified
        self.assertEqual(
            handler_output,
            {
                "Name": "AWS::Include",
                "Parameters": {
                    "Location": {"Fn::Sub": "${S3Bucket}/file.txt"}
                },
            },
        )

        is_local_file_mock.assert_not_called()
        self.s3_uploader_mock.assert_not_called()

    @mock.patch(
        "awscli.customizations.cloudformation.artifact_exporter.is_local_file"
    )
    def test_include_transform_export_handler_non_local_file(
        self, is_local_file_mock
    ):
        # returns unchanged template dict if transform not a local file, and not a S3 URI
        is_local_file_mock.return_value = False

        with self.assertRaises(exceptions.InvalidLocalPathError):
            include_transform_export_handler(
                {
                    "Name": "AWS::Include",
                    "Parameters": {"Location": "http://foo.yaml"},
                },
                self.s3_uploader_mock,
                "parent_dir",
            )
            is_local_file_mock.assert_called_with("http://foo.yaml")
            self.s3_uploader_mock.assert_not_called()

    @mock.patch(
        "awscli.customizations.cloudformation.artifact_exporter.is_local_file"
    )
    def test_include_transform_export_handler_non_include_transform(
        self, is_local_file_mock
    ):
        # ignores transform that is not aws::include
        handler_output = include_transform_export_handler(
            {
                "Name": "AWS::OtherTransform",
                "Parameters": {"Location": "foo.yaml"},
            },
            self.s3_uploader_mock,
            "parent_dir",
        )
        self.s3_uploader_mock.upload_with_dedup.assert_not_called()
        self.assertEqual(
            handler_output,
            {
                "Name": "AWS::OtherTransform",
                "Parameters": {"Location": "foo.yaml"},
            },
        )

    def test_template_export_path_be_folder(self):
        template_path = "/path/foo"
        # Set parent_dir to be a non-existent folder
        with self.assertRaises(ValueError):
            Template(template_path, "somefolder", self.s3_uploader_mock)

        # Set parent_dir to be a real folder, but just a relative path
        with self.make_temp_dir() as dirname:
            with self.assertRaises(ValueError):
                Template(
                    template_path,
                    os.path.relpath(dirname),
                    self.s3_uploader_mock,
                )

    def test_make_zip(self):
        test_file_creator = FileCreator()
        test_file_creator.append_file(
            'index.js',
            'exports handler = (event, context, callback) => {callback(null, event);}',
        )

        dirname = test_file_creator.rootdir

        expected_files = set(['index.js'])

        random_name = ''.join(
            random.choice(string.ascii_letters) for _ in range(10)
        )
        outfile = os.path.join(tempfile.gettempdir(), random_name)

        zipfile_name = None
        try:
            zipfile_name = make_zip(outfile, dirname)

            test_zip_file = zipfile.ZipFile(zipfile_name, "r")
            with closing(test_zip_file) as zf:
                files_in_zip = set()
                for info in zf.infolist():
                    files_in_zip.add(info.filename)

                self.assertEqual(files_in_zip, expected_files)

        finally:
            if zipfile_name:
                os.remove(zipfile_name)
            test_file_creator.remove_all()

    @mock.patch("shutil.copy")
    @mock.patch("tempfile.mkdtemp")
    def test_copy_to_temp_dir(self, mkdtemp_mock, copyfile_mock):
        temp_dir = "/tmp/foo/"
        filename = "test.js"
        mkdtemp_mock.return_value = temp_dir

        returned_dir = copy_to_temp_dir(filename)

        self.assertEqual(returned_dir, temp_dir)
        copyfile_mock.assert_called_once_with(filename, temp_dir + filename)

    @contextmanager
    def make_temp_dir(self):
        filename = tempfile.mkdtemp()
        try:
            yield filename
        finally:
            if filename:
                os.rmdir(filename)

    def example_yaml_template(self):
        return """
        AWSTemplateFormatVersion: '2010-09-09'
        Description: Simple CRUD webservice. State is stored in a SimpleTable (DynamoDB) resource.
        Resources:
        MyFunction:
          Type: AWS::Lambda::Function
          Properties:
            Code: ./handler
            Handler: index.get
            Role:
              Fn::GetAtt:
              - MyFunctionRole
              - Arn
            Timeout: 20
            Runtime: nodejs4.3
        """
