def test_must_collect_errors_and_raise_on_invalid_events(self, SamTemplateMock):

        template_dict = {"a": "b"}
        function_resources = [("id1", "function1"), ("id2", "function2"), ("id3", "function3")]
        api_event_errors = [InvalidEventException("eventid1", "msg"), InvalidEventException("eventid3", "msg"), InvalidEventException("eventid3", "msg")]

        sam_template = Mock()
        SamTemplateMock.return_value = sam_template
        sam_template.set = Mock()
        sam_template.iterate = Mock()
        sam_template.iterate.return_value = function_resources
        self.plugin._get_api_events.return_value = ["1", "2"]
        self.plugin._process_api_events.side_effect = api_event_errors

        with self.assertRaises(InvalidDocumentException) as context:
            self.plugin.on_before_transform_template(template_dict)

        # Verify the content of exception. There are two exceptions embedded one inside another
        #   InvalidDocumentException -> InvalidResourceException -> contains the msg from InvalidEventException
        causes = context.exception.causes
        self.assertEquals(3, len(causes))
        for index, cause in enumerate(causes):
            self.assertTrue(isinstance(cause, InvalidResourceException))

            # Resource's logicalID must be correctly passed
            self.assertEquals(function_resources[index][0], cause._logical_id)

            # Message must directly come from InvalidEventException
            self.assertEquals(api_event_errors[index].message, cause._message)

        # Must cleanup even if there an exception
        self.plugin._maybe_remove_implicit_api.assert_called_with(sam_template)
Example #2
0
    def _process_api_events(self,
                            function,
                            api_events,
                            template,
                            condition=None):
        """
        Actually process given HTTP API events. Iteratively adds the APIs to OpenApi JSON in the respective
        AWS::Serverless::HttpApi resource from the template

        :param SamResource function: SAM Function containing the API events to be processed
        :param dict api_events: Http API Events extracted from the function. These events will be processed
        :param SamTemplate template: SAM Template where AWS::Serverless::HttpApi resources can be found
        :param str condition: optional; this is the condition that is on the function with the API event
        """

        for logicalId, event in api_events.items():
            # api_events only contains HttpApi events
            event_properties = event.get("Properties", {})
            if not event_properties:
                event["Properties"] = event_properties
            self._add_implicit_api_id_if_necessary(event_properties)

            api_id = self._get_api_id(event_properties)
            path = event_properties.get("Path", "")
            method = event_properties.get("Method", "")
            # If no path and method specified, add the $default path and ANY method
            if not path and not method:
                path = "$default"
                method = "x-amazon-apigateway-any-method"
                event_properties["Path"] = path
                event_properties["Method"] = method
            elif not path or not method:
                key = "Path" if not path else "Method"
                raise InvalidEventException(
                    logicalId, "Event is missing key '{}'.".format(key))

            if not isinstance(path, six.string_types) or not isinstance(
                    method, six.string_types):
                key = "Path" if not isinstance(path,
                                               six.string_types) else "Method"
                raise InvalidEventException(
                    logicalId,
                    "Api Event must have a String specified for '{}'.".format(
                        key))

            api_dict = self.api_conditions.setdefault(api_id, {})
            method_conditions = api_dict.setdefault(path, {})

            if condition:
                method_conditions[method] = condition

            self._add_api_to_swagger(logicalId, event_properties, template)
            api_events[logicalId] = event

        # We could have made changes to the Events structure. Write it back to function
        function.properties["Events"].update(api_events)
Example #3
0
    def _process_api_events(self,
                            function,
                            api_events,
                            template,
                            condition=None):
        """
        Actually process given API events. Iteratively adds the APIs to Swagger JSON in the respective Serverless::Api
        resource from the template

        :param SamResource function: SAM Function containing the API events to be processed
        :param dict api_events: API Events extracted from the function. These events will be processed
        :param SamTemplate template: SAM Template where Serverless::Api resources can be found
        :param str condition: optional; this is the condition that is on the function with the API event
        """

        for logicalId, event in api_events.items():

            event_properties = event.get("Properties", {})
            if not event_properties:
                continue

            self._add_implicit_api_id_if_necessary(event_properties)

            api_id = self._get_api_id(event_properties)
            try:
                path = event_properties["Path"]
                method = event_properties["Method"]
            except KeyError as e:
                raise InvalidEventException(
                    logicalId, "Event is missing key {}.".format(e))

            if (not isinstance(path, six.string_types)):
                raise InvalidEventException(
                    logicalId,
                    "Api Event must have a String specified for 'Path'.")
            if (not isinstance(method, six.string_types)):
                raise InvalidEventException(
                    logicalId,
                    "Api Event must have a String specified for 'Method'.")

            api_dict = self.api_conditions.setdefault(api_id, {})
            method_conditions = api_dict.setdefault(path, {})
            method_conditions[method] = condition

            self._add_api_to_swagger(logicalId, event_properties, template)

            api_events[logicalId] = event

        # We could have made changes to the Events structure. Write it back to function
        function.properties["Events"].update(api_events)
    def _add_api_to_swagger(self, event_id, event_properties, template):
        """
        Adds the API path/method from the given event to the Swagger JSON of Serverless::Api resource this event
        refers to.

        :param string event_id: LogicalId of the event
        :param dict event_properties: Properties of the event
        :param SamTemplate template: SAM Template to search for Serverless::Api resources
        """

        # "RestApiId" property of the event contains the logical Id to the AWS::Serverless::Api resource.
        # Need to grab the resource and update Swagger from it
        api_id = event_properties.get("RestApiId")
        if isinstance(api_id, dict) and "Ref" in api_id:
            api_id = api_id["Ref"]

        # RestApiId is not pointing to a valid  API resource
        if isinstance(api_id, dict) or not template.get(api_id):
            raise InvalidEventException(
                event_id,
                "RestApiId must be a valid reference to an 'AWS::Serverless::Api' resource "
                "in same template")

        # Make sure Swagger is valid
        resource = template.get(api_id)
        if not (resource and isinstance(resource.properties, dict)
                and SwaggerEditor.is_valid(
                    resource.properties.get("DefinitionBody"))):
            # This does not have an inline Swagger. Nothing can be done about it.
            return

        if not resource.properties.get("__MANAGE_SWAGGER"):
            # Do not add the api to Swagger, if the resource is not actively managed by SAM.
            # ie. Implicit API resources are created & managed by SAM on behalf of customers.
            # But for explicit API resources, customers write their own Swagger and manage it.
            # If a path is present in Events section but *not* present in the Explicit API Swagger, then it is
            # customer's responsibility to add to Swagger. We will not modify the Swagger here.
            #
            # In the future, we will might expose a flag that will allow SAM to manage explicit API Swagger as well.
            # Until then, we will not modify explicit explicit APIs.
            return

        swagger = resource.properties.get("DefinitionBody")

        path = event_properties["Path"]
        method = event_properties["Method"]
        editor = SwaggerEditor(swagger)
        editor.add_path(path, method)

        resource.properties["DefinitionBody"] = editor.swagger
        template.set(api_id, resource)
Example #5
0
    def _process_api_events(self,
                            function,
                            api_events,
                            template,
                            condition=None,
                            deletion_policy=None,
                            update_replace_policy=None):
        """
        Actually process given API events. Iteratively adds the APIs to Swagger JSON in the respective Serverless::Api
        resource from the template

        :param SamResource function: SAM Function containing the API events to be processed
        :param dict api_events: API Events extracted from the function. These events will be processed
        :param SamTemplate template: SAM Template where Serverless::Api resources can be found
        :param str condition: optional; this is the condition that is on the function with the API event
        """

        for logicalId, event in api_events.items():

            event_properties = event.get("Properties", {})
            if not event_properties:
                continue

            if not isinstance(event_properties, dict):
                raise InvalidEventException(
                    logicalId,
                    "Event 'Properties' must be an Object. If you're using YAML, this may be an indentation issue.",
                )

            self._add_implicit_api_id_if_necessary(event_properties)

            api_id = self._get_api_id(event_properties)
            try:
                path = event_properties["Path"]
                method = event_properties["Method"]
            except KeyError as e:
                raise InvalidEventException(
                    logicalId, "Event is missing key {}.".format(e))

            if not isinstance(path, six.string_types):
                raise InvalidEventException(
                    logicalId,
                    "Api Event must have a String specified for 'Path'.")
            if not isinstance(method, six.string_types):
                raise InvalidEventException(
                    logicalId,
                    "Api Event must have a String specified for 'Method'.")

            # !Ref is resolved by this time. If it is still a dict, we can't parse/use this Api.
            if isinstance(api_id, dict):
                raise InvalidEventException(
                    logicalId,
                    "Api Event must reference an Api in the same template.")

            api_dict_condition = self.api_conditions.setdefault(api_id, {})
            method_conditions = api_dict_condition.setdefault(path, {})
            method_conditions[method] = condition

            api_dict_deletion = self.api_deletion_policies.setdefault(
                api_id, set())
            api_dict_deletion.add(deletion_policy)

            api_dict_update_replace = self.api_update_replace_policies.setdefault(
                api_id, set())
            api_dict_update_replace.add(update_replace_policy)

            self._add_api_to_swagger(logicalId, event_properties, template)

            api_events[logicalId] = event

        # We could have made changes to the Events structure. Write it back to function
        function.properties["Events"].update(api_events)
    def _process_api_events(
        self, function, api_events, template, condition=None, deletion_policy=None, update_replace_policy=None
    ):
        """
        Actually process given HTTP API events. Iteratively adds the APIs to OpenApi JSON in the respective
        AWS::Serverless::HttpApi resource from the template

        :param SamResource function: SAM Function containing the API events to be processed
        :param dict api_events: Http API Events extracted from the function. These events will be processed
        :param SamTemplate template: SAM Template where AWS::Serverless::HttpApi resources can be found
        :param str condition: optional; this is the condition that is on the function with the API event
        """

        for logicalId, event in api_events.items():
            # api_events only contains HttpApi events
            event_properties = event.get("Properties", {})

            if event_properties and not isinstance(event_properties, dict):
                raise InvalidEventException(
                    logicalId,
                    "Event 'Properties' must be an Object. If you're using YAML, this may be an indentation issue.",
                )

            if not event_properties:
                event["Properties"] = event_properties
            self._add_implicit_api_id_if_necessary(event_properties)

            api_id = self._get_api_id(event_properties)
            path = event_properties.get("Path", "")
            method = event_properties.get("Method", "")
            # If no path and method specified, add the $default path and ANY method
            if not path and not method:
                path = "$default"
                method = "x-amazon-apigateway-any-method"
                event_properties["Path"] = path
                event_properties["Method"] = method
            elif not path or not method:
                key = "Path" if not path else "Method"
                raise InvalidEventException(logicalId, "Event is missing key '{}'.".format(key))

            if not isinstance(path, six.string_types) or not isinstance(method, six.string_types):
                key = "Path" if not isinstance(path, six.string_types) else "Method"
                raise InvalidEventException(logicalId, "Api Event must have a String specified for '{}'.".format(key))

            # !Ref is resolved by this time. If it is still a dict, we can't parse/use this Api.
            if isinstance(api_id, dict):
                raise InvalidEventException(logicalId, "Api Event must reference an Api in the same template.")

            api_dict_condition = self.api_conditions.setdefault(api_id, {})
            method_conditions = api_dict_condition.setdefault(path, {})
            method_conditions[method] = condition

            api_dict_deletion = self.api_deletion_policies.setdefault(api_id, set())
            api_dict_deletion.add(deletion_policy)

            api_dict_update_replace = self.api_update_replace_policies.setdefault(api_id, set())
            api_dict_update_replace.add(update_replace_policy)

            self._add_api_to_swagger(logicalId, event_properties, template)
            if "RouteSettings" in event_properties:
                self._add_route_settings_to_api(logicalId, event_properties, template, condition)
            api_events[logicalId] = event

        # We could have made changes to the Events structure. Write it back to function
        function.properties["Events"].update(api_events)