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)
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)
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)
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)