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:
parent
9e1fcac9d4
commit
199da2220b
@ -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
|
||||
|
||||
|
@ -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"):
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
107
tests/test_events/test_events_cloudformation.py
Normal file
107
tests/test_events/test_events_cloudformation.py
Normal 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)
|
Loading…
Reference in New Issue
Block a user