Add EventBridge Archive CRUD endpoints (#3617)

* Add events.create_archive

* Add events.describe_archive

* Add events.list_archives

* Add events.update_archive

* Add events.delete_archive

* Add actual archive functionality

* Fix test

* Fix PR issues
This commit is contained in:
Anton Grübel 2021-01-28 11:47:53 +01:00 committed by GitHub
parent 9e1fcac9d4
commit 199da2220b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 985 additions and 1 deletions

View File

@ -2,6 +2,15 @@ from __future__ import unicode_literals
from moto.core.exceptions import JsonRESTError 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): class ResourceNotFoundException(JsonRESTError):
code = 400 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): class ValidationException(JsonRESTError):
code = 400 code = 400

View File

@ -1,11 +1,21 @@
import copy
import os import os
import re import re
import json import json
import sys
from datetime import datetime
from boto3 import Session from boto3 import Session
from moto.core.exceptions import JsonRESTError from moto.core.exceptions import JsonRESTError
from moto.core import ACCOUNT_ID, BaseBackend, CloudFormationModel 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 moto.utilities.tagging_service import TaggingService
from uuid import uuid4 from uuid import uuid4
@ -200,6 +210,146 @@ class EventBus(CloudFormationModel):
event_backend.delete_event_bus(event_bus_name) 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): class EventsBackend(BaseBackend):
ACCOUNT_ID = re.compile(r"^(\d{1,12}|\*)$") ACCOUNT_ID = re.compile(r"^(\d{1,12}|\*)$")
STATEMENT_ID = re.compile(r"^[a-zA-Z0-9-_]{1,64}$") STATEMENT_ID = re.compile(r"^[a-zA-Z0-9-_]{1,64}$")
@ -213,6 +363,7 @@ class EventsBackend(BaseBackend):
self.region_name = region_name self.region_name = region_name
self.event_buses = {} self.event_buses = {}
self.event_sources = {} self.event_sources = {}
self.archives = {}
self.tagger = TaggingService() self.tagger = TaggingService()
self._add_default_event_bus() self._add_default_event_bus()
@ -404,6 +555,22 @@ class EventsBackend(BaseBackend):
entries.append({"EventId": str(uuid4())}) 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 # We dont really need to store the events yet
return entries return entries
@ -544,6 +711,118 @@ class EventsBackend(BaseBackend):
"Rule {0} does not exist on EventBus default.".format(name) "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 = {} events_backends = {}
for region in Session().get_available_regions("events"): for region in Session().get_available_regions("events"):

View File

@ -332,3 +332,60 @@ class EventsHandler(BaseResponse):
result = self.events_backend.untag_resource(arn, tags) result = self.events_backend.untag_resource(arn, tags)
return json.dumps(result), self.response_headers 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

View File

@ -1,6 +1,7 @@
import json import json
import random import random
import unittest import unittest
from datetime import datetime
import boto3 import boto3
import sure # noqa import sure # noqa
@ -748,3 +749,525 @@ def test_list_tags_for_resource_error_unknown_arn():
ex.response["Error"]["Message"].should.equal( ex.response["Error"]["Message"].should.equal(
"Rule unknown does not exist on EventBus default." "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)

View File

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