diff --git a/moto/events/exceptions.py b/moto/events/exceptions.py index ee5ea7641..e31bda09b 100644 --- a/moto/events/exceptions.py +++ b/moto/events/exceptions.py @@ -2,6 +2,15 @@ from __future__ import unicode_literals from moto.core.exceptions import JsonRESTError +class InvalidEventPatternException(JsonRESTError): + code = 400 + + def __init__(self): + super(InvalidEventPatternException, self).__init__( + "InvalidEventPatternException", "Event pattern is not valid." + ) + + class ResourceNotFoundException(JsonRESTError): code = 400 @@ -11,6 +20,15 @@ class ResourceNotFoundException(JsonRESTError): ) +class ResourceAlreadyExistsException(JsonRESTError): + code = 400 + + def __init__(self, message): + super(ResourceAlreadyExistsException, self).__init__( + "ResourceAlreadyExistsException", message + ) + + class ValidationException(JsonRESTError): code = 400 diff --git a/moto/events/models.py b/moto/events/models.py index 7dfa2c691..7d9b4d177 100644 --- a/moto/events/models.py +++ b/moto/events/models.py @@ -1,11 +1,21 @@ +import copy import os import re import json +import sys +from datetime import datetime + from boto3 import Session from moto.core.exceptions import JsonRESTError from moto.core import ACCOUNT_ID, BaseBackend, CloudFormationModel -from moto.events.exceptions import ValidationException, ResourceNotFoundException +from moto.core.utils import unix_time +from moto.events.exceptions import ( + ValidationException, + ResourceNotFoundException, + ResourceAlreadyExistsException, + InvalidEventPatternException, +) from moto.utilities.tagging_service import TaggingService from uuid import uuid4 @@ -200,6 +210,146 @@ class EventBus(CloudFormationModel): event_backend.delete_event_bus(event_bus_name) +class Archive(CloudFormationModel): + # https://docs.aws.amazon.com/eventbridge/latest/APIReference/API_ListArchives.html#API_ListArchives_RequestParameters + VALID_STATES = [ + "ENABLED", + "DISABLED", + "CREATING", + "UPDATING", + "CREATE_FAILED", + "UPDATE_FAILED", + ] + + def __init__( + self, region_name, name, source_arn, description, event_pattern, retention + ): + self.region = region_name + self.name = name + self.source_arn = source_arn + self.description = description + self.event_pattern = event_pattern + self.retention = retention if retention else 0 + + self.creation_time = unix_time(datetime.utcnow()) + self.state = "ENABLED" + + self.events = [] + self.event_bus_name = source_arn.split("/")[-1] + + @property + def arn(self): + return "arn:aws:events:{region}:{account_id}:archive/{name}".format( + region=self.region, account_id=ACCOUNT_ID, name=self.name + ) + + def describe_short(self): + return { + "ArchiveName": self.name, + "EventSourceArn": self.source_arn, + "State": self.state, + "RetentionDays": self.retention, + "SizeBytes": sys.getsizeof(self.events) if len(self.events) > 0 else 0, + "EventCount": len(self.events), + "CreationTime": self.creation_time, + } + + def describe(self): + result = { + "ArchiveArn": self.arn, + "Description": self.description, + "EventPattern": self.event_pattern, + } + result.update(self.describe_short()) + + return result + + def update(self, description, event_pattern, retention): + if description: + self.description = description + if event_pattern: + self.event_pattern = event_pattern + if retention: + self.retention = retention + + def delete(self, region_name): + event_backend = events_backends[region_name] + event_backend.archives.pop(self.name) + + def matches_pattern(self, event): + if not self.event_pattern: + return True + + # only works on the first level of the event dict + # logic for nested dicts needs to be implemented + for pattern_key, pattern_value in json.loads(self.event_pattern).items(): + event_value = event.get(pattern_key) + if event_value not in pattern_value: + return False + + def get_cfn_attribute(self, attribute_name): + from moto.cloudformation.exceptions import UnformattedGetAttTemplateException + + if attribute_name == "ArchiveName": + return self.name + elif attribute_name == "Arn": + return self.arn + + raise UnformattedGetAttTemplateException() + + @staticmethod + def cloudformation_name_type(): + return "ArchiveName" + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-events-archive.html + return "AWS::Events::Archive" + + @classmethod + def create_from_cloudformation_json( + cls, resource_name, cloudformation_json, region_name + ): + properties = cloudformation_json["Properties"] + event_backend = events_backends[region_name] + + source_arn = properties.get("SourceArn") + description = properties.get("Description") + event_pattern = properties.get("EventPattern") + retention = properties.get("RetentionDays") + + return event_backend.create_archive( + resource_name, source_arn, description, event_pattern, retention + ) + + @classmethod + def update_from_cloudformation_json( + cls, original_resource, new_resource_name, cloudformation_json, region_name + ): + if new_resource_name == original_resource.name: + properties = cloudformation_json["Properties"] + + original_resource.update( + properties.get("Description"), + properties.get("EventPattern"), + properties.get("Retention"), + ) + + return original_resource + else: + original_resource.delete(region_name) + return cls.create_from_cloudformation_json( + new_resource_name, cloudformation_json, region_name + ) + + @classmethod + def delete_from_cloudformation_json( + cls, resource_name, cloudformation_json, region_name + ): + event_backend = events_backends[region_name] + event_backend.delete_archive(resource_name) + + class EventsBackend(BaseBackend): ACCOUNT_ID = re.compile(r"^(\d{1,12}|\*)$") STATEMENT_ID = re.compile(r"^[a-zA-Z0-9-_]{1,64}$") @@ -213,6 +363,7 @@ class EventsBackend(BaseBackend): self.region_name = region_name self.event_buses = {} self.event_sources = {} + self.archives = {} self.tagger = TaggingService() self._add_default_event_bus() @@ -404,6 +555,22 @@ class EventsBackend(BaseBackend): entries.append({"EventId": str(uuid4())}) + # add to correct archive + # if 'EventBusName' is not espically set, it will stored in the default + event_bus_name = event.get("EventBusName", "default") + archives = [ + archive + for archive in self.archives.values() + if archive.event_bus_name == event_bus_name + ] + + for archive in archives: + event_copy = copy.deepcopy(event) + event_copy.pop("EventBusName", None) + + if archive.matches_pattern(event): + archive.events.append(event_copy) + # We dont really need to store the events yet return entries @@ -544,6 +711,118 @@ class EventsBackend(BaseBackend): "Rule {0} does not exist on EventBus default.".format(name) ) + def create_archive(self, name, source_arn, description, event_pattern, retention): + if len(name) > 48: + raise ValidationException( + " 1 validation error detected: " + "Value '{}' at 'archiveName' failed to satisfy constraint: " + "Member must have length less than or equal to 48".format(name) + ) + + if event_pattern: + self._validate_event_pattern(event_pattern) + + event_bus_name = source_arn.split("/")[-1] + if event_bus_name not in self.event_buses: + raise ResourceNotFoundException( + "Event bus {} does not exist.".format(event_bus_name) + ) + + if name in self.archives: + raise ResourceAlreadyExistsException( + "Archive {} already exists.".format(name) + ) + + archive = Archive( + self.region_name, name, source_arn, description, event_pattern, retention + ) + + self.archives[name] = archive + + return archive + + def _validate_event_pattern(self, pattern): + try: + json_pattern = json.loads(pattern) + except ValueError: # json.JSONDecodeError exists since Python 3.5 + raise InvalidEventPatternException + + if not self._is_event_value_an_array(json_pattern): + raise InvalidEventPatternException + + def _is_event_value_an_array(self, pattern): + # the values of a key in the event pattern have to be either a dict or an array + for value in pattern.values(): + if isinstance(value, dict): + if not self._is_event_value_an_array(value): + return False + elif not isinstance(value, list): + return False + + return True + + def describe_archive(self, name): + archive = self.archives.get(name) + + if not archive: + raise ResourceNotFoundException("Archive {} does not exist.".format(name)) + + return archive.describe() + + def list_archives(self, name_prefix, source_arn, state): + if [name_prefix, source_arn, state].count(None) < 2: + raise ValidationException( + "At most one filter is allowed for ListArchives. " + "Use either : State, EventSourceArn, or NamePrefix." + ) + + if state and state not in Archive.VALID_STATES: + raise ValidationException( + "1 validation error detected: Value '{0}' at 'state' failed to satisfy constraint: " + "Member must satisfy enum value set: " + "[{1}]".format(state, ", ".join(Archive.VALID_STATES)) + ) + + if [name_prefix, source_arn, state].count(None) == 3: + return [archive.describe_short() for archive in self.archives.values()] + + result = [] + + for archive in self.archives.values(): + if name_prefix and archive.name.startswith(name_prefix): + result.append(archive.describe_short()) + elif source_arn and archive.source_arn == source_arn: + result.append(archive.describe_short()) + elif state and archive.state == state: + result.append(archive.describe_short()) + + return result + + def update_archive(self, name, description, event_pattern, retention): + archive = self.archives.get(name) + + if not archive: + raise ResourceNotFoundException("Archive {} does not exist.".format(name)) + + if event_pattern: + self._validate_event_pattern(event_pattern) + + archive.update(description, event_pattern, retention) + + return { + "ArchiveArn": archive.arn, + "CreationTime": archive.creation_time, + "State": archive.state, + } + + def delete_archive(self, name): + archive = self.archives.get(name) + + if not archive: + raise ResourceNotFoundException("Archive {} does not exist.".format(name)) + + archive.delete(self.region_name) + events_backends = {} for region in Session().get_available_regions("events"): diff --git a/moto/events/responses.py b/moto/events/responses.py index 99577bacb..d1f37b47b 100644 --- a/moto/events/responses.py +++ b/moto/events/responses.py @@ -332,3 +332,60 @@ class EventsHandler(BaseResponse): result = self.events_backend.untag_resource(arn, tags) return json.dumps(result), self.response_headers + + def create_archive(self): + name = self._get_param("ArchiveName") + source_arn = self._get_param("EventSourceArn") + description = self._get_param("Description") + event_pattern = self._get_param("EventPattern") + retention = self._get_param("RetentionDays") + + archive = self.events_backend.create_archive( + name, source_arn, description, event_pattern, retention + ) + + return ( + json.dumps( + { + "ArchiveArn": archive.arn, + "CreationTime": archive.creation_time, + "State": archive.state, + } + ), + self.response_headers, + ) + + def describe_archive(self): + name = self._get_param("ArchiveName") + + result = self.events_backend.describe_archive(name) + + return json.dumps(result), self.response_headers + + def list_archives(self): + name_prefix = self._get_param("NamePrefix") + source_arn = self._get_param("EventSourceArn") + state = self._get_param("State") + + result = self.events_backend.list_archives(name_prefix, source_arn, state) + + return json.dumps({"Archives": result}), self.response_headers + + def update_archive(self): + name = self._get_param("ArchiveName") + description = self._get_param("Description") + event_pattern = self._get_param("EventPattern") + retention = self._get_param("RetentionDays") + + result = self.events_backend.update_archive( + name, description, event_pattern, retention + ) + + return json.dumps(result), self.response_headers + + def delete_archive(self): + name = self._get_param("ArchiveName") + + self.events_backend.delete_archive(name) + + return "", self.response_headers diff --git a/tests/test_events/test_events.py b/tests/test_events/test_events.py index 63b1b0db7..8907fc752 100644 --- a/tests/test_events/test_events.py +++ b/tests/test_events/test_events.py @@ -1,6 +1,7 @@ import json import random import unittest +from datetime import datetime import boto3 import sure # noqa @@ -748,3 +749,525 @@ def test_list_tags_for_resource_error_unknown_arn(): ex.response["Error"]["Message"].should.equal( "Rule unknown does not exist on EventBus default." ) + + +@mock_events +def test_create_archive(): + # given + client = boto3.client("events", "eu-central-1") + + # when + response = client.create_archive( + ArchiveName="test-archive", + EventSourceArn="arn:aws:events:eu-central-1:{}:event-bus/default".format( + ACCOUNT_ID + ), + ) + + # then + response["ArchiveArn"].should.equal( + "arn:aws:events:eu-central-1:{}:archive/test-archive".format(ACCOUNT_ID) + ) + response["CreationTime"].should.be.a(datetime) + response["State"].should.equal("ENABLED") + + +@mock_events +def test_create_archive_custom_event_bus(): + # given + client = boto3.client("events", "eu-central-1") + event_bus_arn = client.create_event_bus(Name="test-bus")["EventBusArn"] + + # when + response = client.create_archive( + ArchiveName="test-archive", + EventSourceArn=event_bus_arn, + EventPattern=json.dumps( + { + "key_1": { + "key_2": {"key_3": ["value_1", "value_2"], "key_4": ["value_3"]} + } + } + ), + ) + + # then + response["ArchiveArn"].should.equal( + "arn:aws:events:eu-central-1:{}:archive/test-archive".format(ACCOUNT_ID) + ) + response["CreationTime"].should.be.a(datetime) + response["State"].should.equal("ENABLED") + + +@mock_events +def test_create_archive_error_long_name(): + # given + client = boto3.client("events", "eu-central-1") + name = "a" * 49 + + # when + with pytest.raises(ClientError) as e: + client.create_archive( + ArchiveName=name, + EventSourceArn=( + "arn:aws:events:eu-central-1:{}:event-bus/default".format(ACCOUNT_ID) + ), + ) + + # then + ex = e.value + ex.operation_name.should.equal("CreateArchive") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("ValidationException") + ex.response["Error"]["Message"].should.equal( + " 1 validation error detected: " + "Value '{}' at 'archiveName' failed to satisfy constraint: " + "Member must have length less than or equal to 48".format(name) + ) + + +@mock_events +def test_create_archive_error_invalid_event_pattern(): + # given + client = boto3.client("events", "eu-central-1") + + # when + with pytest.raises(ClientError) as e: + client.create_archive( + ArchiveName="test-archive", + EventSourceArn=( + "arn:aws:events:eu-central-1:{}:event-bus/default".format(ACCOUNT_ID) + ), + EventPattern="invalid", + ) + + # then + ex = e.value + ex.operation_name.should.equal("CreateArchive") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("InvalidEventPatternException") + ex.response["Error"]["Message"].should.equal("Event pattern is not valid.") + + +@mock_events +def test_create_archive_error_invalid_event_pattern_not_an_array(): + # given + client = boto3.client("events", "eu-central-1") + + # when + with pytest.raises(ClientError) as e: + client.create_archive( + ArchiveName="test-archive", + EventSourceArn=( + "arn:aws:events:eu-central-1:{}:event-bus/default".format(ACCOUNT_ID) + ), + EventPattern=json.dumps( + { + "key_1": { + "key_2": {"key_3": ["value_1"]}, + "key_4": {"key_5": ["value_2"], "key_6": "value_3"}, + } + } + ), + ) + + # then + ex = e.value + ex.operation_name.should.equal("CreateArchive") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("InvalidEventPatternException") + ex.response["Error"]["Message"].should.equal("Event pattern is not valid.") + + +@mock_events +def test_create_archive_error_unknown_event_bus(): + # given + client = boto3.client("events", "eu-central-1") + event_bus_name = "unknown" + + # when + with pytest.raises(ClientError) as e: + client.create_archive( + ArchiveName="test-archive", + EventSourceArn=( + "arn:aws:events:eu-central-1:{}:event-bus/{}".format( + ACCOUNT_ID, event_bus_name + ) + ), + ) + + # then + ex = e.value + ex.operation_name.should.equal("CreateArchive") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("ResourceNotFoundException") + ex.response["Error"]["Message"].should.equal( + "Event bus {} does not exist.".format(event_bus_name) + ) + + +@mock_events +def test_create_archive_error_duplicate(): + # given + client = boto3.client("events", "eu-central-1") + name = "test-archive" + source_arn = "arn:aws:events:eu-central-1:{}:event-bus/default".format(ACCOUNT_ID) + client.create_archive(ArchiveName=name, EventSourceArn=source_arn) + + # when + with pytest.raises(ClientError) as e: + client.create_archive(ArchiveName=name, EventSourceArn=source_arn) + + # then + ex = e.value + ex.operation_name.should.equal("CreateArchive") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("ResourceAlreadyExistsException") + ex.response["Error"]["Message"].should.equal("Archive test-archive already exists.") + + +@mock_events +def test_describe_archive(): + # given + client = boto3.client("events", "eu-central-1") + name = "test-archive" + source_arn = "arn:aws:events:eu-central-1:{}:event-bus/default".format(ACCOUNT_ID) + event_pattern = json.dumps({"key": ["value"]}) + client.create_archive( + ArchiveName=name, + EventSourceArn=source_arn, + Description="test archive", + EventPattern=event_pattern, + ) + + # when + response = client.describe_archive(ArchiveName=name) + + # then + response["ArchiveArn"].should.equal( + "arn:aws:events:eu-central-1:{0}:archive/{1}".format(ACCOUNT_ID, name) + ) + response["ArchiveName"].should.equal(name) + response["CreationTime"].should.be.a(datetime) + response["Description"].should.equal("test archive") + response["EventCount"].should.equal(0) + response["EventPattern"].should.equal(event_pattern) + response["EventSourceArn"].should.equal(source_arn) + response["RetentionDays"].should.equal(0) + response["SizeBytes"].should.equal(0) + response["State"].should.equal("ENABLED") + + +@mock_events +def test_describe_archive_error_unknown_archive(): + # given + client = boto3.client("events", "eu-central-1") + name = "unknown" + + # when + with pytest.raises(ClientError) as e: + client.describe_archive(ArchiveName=name) + + # then + ex = e.value + ex.operation_name.should.equal("DescribeArchive") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("ResourceNotFoundException") + ex.response["Error"]["Message"].should.equal( + "Archive {} does not exist.".format(name) + ) + + +@mock_events +def test_list_archives(): + # given + client = boto3.client("events", "eu-central-1") + name = "test-archive" + source_arn = "arn:aws:events:eu-central-1:{}:event-bus/default".format(ACCOUNT_ID) + event_pattern = json.dumps({"key": ["value"]}) + client.create_archive( + ArchiveName=name, + EventSourceArn=source_arn, + Description="test archive", + EventPattern=event_pattern, + ) + + # when + archives = client.list_archives()["Archives"] + + # then + archives.should.have.length_of(1) + archive = archives[0] + archive["ArchiveName"].should.equal(name) + archive["CreationTime"].should.be.a(datetime) + archive["EventCount"].should.equal(0) + archive["EventSourceArn"].should.equal(source_arn) + archive["RetentionDays"].should.equal(0) + archive["SizeBytes"].should.equal(0) + archive["State"].should.equal("ENABLED") + + archive.should_not.have.key("ArchiveArn") + archive.should_not.have.key("Description") + archive.should_not.have.key("EventPattern") + + +@mock_events +def test_list_archives_with_name_prefix(): + # given + client = boto3.client("events", "eu-central-1") + source_arn = "arn:aws:events:eu-central-1:{}:event-bus/default".format(ACCOUNT_ID) + client.create_archive( + ArchiveName="test", EventSourceArn=source_arn, + ) + client.create_archive( + ArchiveName="test-archive", EventSourceArn=source_arn, + ) + + # when + archives = client.list_archives(NamePrefix="test-")["Archives"] + + # then + archives.should.have.length_of(1) + archives[0]["ArchiveName"].should.equal("test-archive") + + +@mock_events +def test_list_archives_with_source_arn(): + # given + client = boto3.client("events", "eu-central-1") + source_arn = "arn:aws:events:eu-central-1:{}:event-bus/default".format(ACCOUNT_ID) + source_arn_2 = client.create_event_bus(Name="test-bus")["EventBusArn"] + client.create_archive( + ArchiveName="test", EventSourceArn=source_arn, + ) + client.create_archive( + ArchiveName="test-archive", EventSourceArn=source_arn_2, + ) + + # when + archives = client.list_archives(EventSourceArn=source_arn)["Archives"] + + # then + archives.should.have.length_of(1) + archives[0]["ArchiveName"].should.equal("test") + + +@mock_events +def test_list_archives_with_state(): + # given + client = boto3.client("events", "eu-central-1") + source_arn = "arn:aws:events:eu-central-1:{}:event-bus/default".format(ACCOUNT_ID) + client.create_archive( + ArchiveName="test", EventSourceArn=source_arn, + ) + client.create_archive( + ArchiveName="test-archive", EventSourceArn=source_arn, + ) + + # when + archives = client.list_archives(State="DISABLED")["Archives"] + + # then + archives.should.have.length_of(0) + + +@mock_events +def test_list_archives_error_multiple_filters(): + # given + client = boto3.client("events", "eu-central-1") + + # when + with pytest.raises(ClientError) as e: + client.list_archives(NamePrefix="test", State="ENABLED") + + # then + ex = e.value + ex.operation_name.should.equal("ListArchives") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("ValidationException") + ex.response["Error"]["Message"].should.equal( + "At most one filter is allowed for ListArchives. " + "Use either : State, EventSourceArn, or NamePrefix." + ) + + +@mock_events +def test_list_archives_error_invalid_state(): + # given + client = boto3.client("events", "eu-central-1") + + # when + with pytest.raises(ClientError) as e: + client.list_archives(State="invalid") + + # then + ex = e.value + ex.operation_name.should.equal("ListArchives") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("ValidationException") + ex.response["Error"]["Message"].should.equal( + "1 validation error detected: Value 'invalid' at 'state' failed to satisfy constraint: " + "Member must satisfy enum value set: " + "[ENABLED, DISABLED, CREATING, UPDATING, CREATE_FAILED, UPDATE_FAILED]" + ) + + +@mock_events +def test_update_archive(): + # given + client = boto3.client("events", "eu-central-1") + name = "test-archive" + source_arn = "arn:aws:events:eu-central-1:{}:event-bus/default".format(ACCOUNT_ID) + event_pattern = json.dumps({"key": ["value"]}) + archive_arn = client.create_archive(ArchiveName=name, EventSourceArn=source_arn)[ + "ArchiveArn" + ] + + # when + response = client.update_archive( + ArchiveName=name, + Description="test archive", + EventPattern=event_pattern, + RetentionDays=14, + ) + + # then + response["ArchiveArn"].should.equal(archive_arn) + response["State"].should.equal("ENABLED") + creation_time = response["CreationTime"] + creation_time.should.be.a(datetime) + + response = client.describe_archive(ArchiveName=name) + response["ArchiveArn"].should.equal(archive_arn) + response["ArchiveName"].should.equal(name) + response["CreationTime"].should.equal(creation_time) + response["Description"].should.equal("test archive") + response["EventCount"].should.equal(0) + response["EventPattern"].should.equal(event_pattern) + response["EventSourceArn"].should.equal(source_arn) + response["RetentionDays"].should.equal(14) + response["SizeBytes"].should.equal(0) + response["State"].should.equal("ENABLED") + + +@mock_events +def test_update_archive_error_invalid_event_pattern(): + # given + client = boto3.client("events", "eu-central-1") + name = "test-archive" + client.create_archive( + ArchiveName=name, + EventSourceArn="arn:aws:events:eu-central-1:{}:event-bus/default".format( + ACCOUNT_ID + ), + ) + + # when + with pytest.raises(ClientError) as e: + client.update_archive( + ArchiveName=name, EventPattern="invalid", + ) + + # then + ex = e.value + ex.operation_name.should.equal("UpdateArchive") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("InvalidEventPatternException") + ex.response["Error"]["Message"].should.equal("Event pattern is not valid.") + + +@mock_events +def test_update_archive_error_unknown_archive(): + # given + client = boto3.client("events", "eu-central-1") + name = "unknown" + + # when + with pytest.raises(ClientError) as e: + client.update_archive(ArchiveName=name) + + # then + ex = e.value + ex.operation_name.should.equal("UpdateArchive") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("ResourceNotFoundException") + ex.response["Error"]["Message"].should.equal( + "Archive {} does not exist.".format(name) + ) + + +@mock_events +def test_delete_archive(): + # given + client = boto3.client("events", "eu-central-1") + name = "test-archive" + client.create_archive( + ArchiveName=name, + EventSourceArn="arn:aws:events:eu-central-1:{}:event-bus/default".format( + ACCOUNT_ID + ), + ) + + # when + client.delete_archive(ArchiveName=name) + + # then + response = client.list_archives(NamePrefix="test")["Archives"] + response.should.have.length_of(0) + + +@mock_events +def test_delete_archive_error_unknown_archive(): + # given + client = boto3.client("events", "eu-central-1") + name = "unknown" + + # when + with pytest.raises(ClientError) as e: + client.delete_archive(ArchiveName=name) + + # then + ex = e.value + ex.operation_name.should.equal("DeleteArchive") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("ResourceNotFoundException") + ex.response["Error"]["Message"].should.equal( + "Archive {} does not exist.".format(name) + ) + + +@mock_events +def test_archive_actual_events(): + # given + client = boto3.client("events", "eu-central-1") + name = "test-archive" + name_2 = "test-archive-2" + event_bus_arn = "arn:aws:events:eu-central-1:{}:event-bus/default".format( + ACCOUNT_ID + ) + event = { + "Source": "source", + "DetailType": "type", + "Detail": '{ "key1": "value1" }', + } + client.create_archive(ArchiveName=name, EventSourceArn=event_bus_arn) + client.create_archive( + ArchiveName=name_2, + EventSourceArn=event_bus_arn, + EventPattern=json.dumps({"DetailType": ["type"], "Source": ["test"]}), + ) + + # when + response = client.put_events(Entries=[event]) + + # then + response["FailedEntryCount"].should.equal(0) + response["Entries"].should.have.length_of(1) + + response = client.describe_archive(ArchiveName=name) + response["EventCount"].should.equal(1) + response["SizeBytes"].should.be.greater_than(0) + + response = client.describe_archive(ArchiveName=name_2) + response["EventCount"].should.equal(0) + response["SizeBytes"].should.equal(0) diff --git a/tests/test_events/test_events_cloudformation.py b/tests/test_events/test_events_cloudformation.py new file mode 100644 index 000000000..b22f37837 --- /dev/null +++ b/tests/test_events/test_events_cloudformation.py @@ -0,0 +1,107 @@ +import copy +from string import Template + +import boto3 +import json +from moto import mock_cloudformation, mock_events +import sure # noqa + +from moto.core import ACCOUNT_ID + +archive_template = Template( + json.dumps( + { + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "EventBridge Archive Test", + "Resources": { + "Archive": { + "Type": "AWS::Events::Archive", + "Properties": { + "ArchiveName": "${archive_name}", + "SourceArn": { + "Fn::Sub": "arn:aws:events:$${AWS::Region}:$${AWS::AccountId}:event-bus/default" + }, + }, + } + }, + "Outputs": { + "Arn": { + "Description": "Archive Arn", + "Value": {"Fn::GetAtt": ["Archive", "Arn"]}, + } + }, + } + ) +) + + +@mock_events +@mock_cloudformation +def test_create_archive(): + # given + cfn_client = boto3.client("cloudformation", region_name="eu-central-1") + name = "test-archive" + stack_name = "test-stack" + template = archive_template.substitute({"archive_name": name}) + + # when + cfn_client.create_stack(StackName=stack_name, TemplateBody=template) + + # then + archive_arn = "arn:aws:events:eu-central-1:{0}:archive/{1}".format(ACCOUNT_ID, name) + stack = cfn_client.describe_stacks(StackName=stack_name)["Stacks"][0] + stack["Outputs"][0]["OutputValue"].should.equal(archive_arn) + + events_client = boto3.client("events", region_name="eu-central-1") + response = events_client.describe_archive(ArchiveName=name) + + response["ArchiveArn"].should.equal(archive_arn) + + +@mock_events +@mock_cloudformation +def test_update_archive(): + # given + cfn_client = boto3.client("cloudformation", region_name="eu-central-1") + name = "test-archive" + stack_name = "test-stack" + template = archive_template.substitute({"archive_name": name}) + cfn_client.create_stack(StackName=stack_name, TemplateBody=template) + + template_update = copy.deepcopy(json.loads(template)) + template_update["Resources"]["Archive"]["Properties"][ + "Description" + ] = "test archive" + + # when + cfn_client.update_stack( + StackName=stack_name, TemplateBody=json.dumps(template_update) + ) + + # then + events_client = boto3.client("events", region_name="eu-central-1") + response = events_client.describe_archive(ArchiveName=name) + + response["ArchiveArn"].should.equal( + "arn:aws:events:eu-central-1:{0}:archive/{1}".format(ACCOUNT_ID, name) + ) + response["Description"].should.equal("test archive") + + +@mock_events +@mock_cloudformation +def test_delete_archive(): + # given + cfn_client = boto3.client("cloudformation", region_name="eu-central-1") + name = "test-archive" + stack_name = "test-stack" + template = archive_template.substitute({"archive_name": name}) + cfn_client.create_stack(StackName=stack_name, TemplateBody=template) + + # when + cfn_client.delete_stack(StackName=stack_name) + + # then + events_client = boto3.client("events", region_name="eu-central-1") + response = events_client.list_archives(NamePrefix="test")["Archives"] + response.should.have.length_of(0)