From d0a66f2dffaabfb50595b357aa70a74c19f44fc9 Mon Sep 17 00:00:00 2001 From: Justin Wiley Date: Tue, 29 Nov 2016 17:19:26 -0800 Subject: [PATCH 1/7] Begin work on mocking CloudWatch Events. --- moto/events/__init__.py | 5 ++++ moto/events/models.py | 58 +++++++++++++++++++++++++++++++++++++++ moto/events/responses.py | 59 ++++++++++++++++++++++++++++++++++++++++ moto/events/urls.py | 11 ++++++++ 4 files changed, 133 insertions(+) create mode 100644 moto/events/__init__.py create mode 100644 moto/events/models.py create mode 100644 moto/events/responses.py create mode 100644 moto/events/urls.py diff --git a/moto/events/__init__.py b/moto/events/__init__.py new file mode 100644 index 00000000..8b15e852 --- /dev/null +++ b/moto/events/__init__.py @@ -0,0 +1,5 @@ +from __future__ import unicode_literals + +from .models import events_backend + +mock_events = events_backend.decorator diff --git a/moto/events/models.py b/moto/events/models.py new file mode 100644 index 00000000..f732a55a --- /dev/null +++ b/moto/events/models.py @@ -0,0 +1,58 @@ +from moto.core import BaseBackend + + +class EventsBackend(BaseBackend): + + def __init__(self): + self.events = {} + self.rules = {} + + def can_paginate(self): + pass + + def delete_rule(self): + pass + + def describe_rule(self, name): + event = self.events['name'] + + def disable_rule(self): + pass + + def enable_rule(self): + pass + + def generate_presigned_url(self): + pass + + def get_paginator(self): + pass + + def get_waiter(self): + pass + + def list_rule_names_by_target(self): + pass + + def list_rules(self): + pass + + def list_targets_by_rule(self): + pass + + def put_events(self): + pass + + def put_rule(self, name, **kwargs): + pass + + def put_targets(self): + pass + + def remove_targets(self): + pass + + def test_event_pattern(self): + pass + +events_backend = EventsBackend() diff --git a/moto/events/responses.py b/moto/events/responses.py new file mode 100644 index 00000000..7be87d03 --- /dev/null +++ b/moto/events/responses.py @@ -0,0 +1,59 @@ +import json + +from moto.core.responses import BaseResponse + + +class EventsHandler(BaseResponse): + + def error(self, type_, status=400): + return status, self.response_headers, json.dumps({'__type': type_}) + + def can_paginate(self): + pass + + def delete_rule(self): + pass + + def describe_rule(self): + pass + + def disable_rule(self): + pass + + def enable_rule(self): + pass + + def generate_presigned_url(self): + pass + + def get_paginator(self): + pass + + def get_waiter(self): + pass + + def list_rule_names_by_target(self): + pass + + def list_rules(self): + pass + + def list_targets_by_rule(self): + pass + + def put_events(self): + pass + + def put_rule(self): + if 'Name' not in self.body: + return self.error("com.amazonaws.events.validate#ValidationException") + pass + + def put_targets(self): + pass + + def remove_targets(self): + pass + + def test_event_pattern(self): + pass diff --git a/moto/events/urls.py b/moto/events/urls.py new file mode 100644 index 00000000..9484e385 --- /dev/null +++ b/moto/events/urls.py @@ -0,0 +1,11 @@ +from __future__ import unicode_literals + +from .responses import EventsHandler + +url_bases = [ + "https?://events.(.+).amazonaws.com" +] + +url_paths = { + "{0}/": EventsHandler.dispatch, +} \ No newline at end of file From d0def03c4ccfe0901c039d0bc222533959aee4d8 Mon Sep 17 00:00:00 2001 From: Justin Wiley Date: Wed, 30 Nov 2016 17:09:58 -0800 Subject: [PATCH 2/7] Events models first draft done, need to write tests, then get responses going. --- moto/events/models.py | 172 +++++++++++++++++++++++++++++++++------ moto/events/responses.py | 6 +- 2 files changed, 148 insertions(+), 30 deletions(-) diff --git a/moto/events/models.py b/moto/events/models.py index f732a55a..5a10c165 100644 --- a/moto/events/models.py +++ b/moto/events/models.py @@ -1,58 +1,176 @@ +import binascii +import os +import re +from collections import OrderedDict + from moto.core import BaseBackend +class Rule(object): + + def _generate_arn(self, name): + return 'arn:aws:events:us-west-2:111111111111:rule/' + name + + def __init__(self, name, **kwargs): + self.name = name + self.arn = kwargs['arn'] if 'arn' in kwargs else self._generate_arn(name) + self.event_pattern = kwargs['event_pattern'] if 'event_pattern' in kwargs else None + self.schedule_exp = kwargs['schedule_exp'] if 'schedule_exp' in kwargs else None + self.state = kwargs['state'] if 'state' in kwargs else 'ENABLED' + self.description = kwargs['description'] if 'description' in kwargs else None + self.role_arn = kwargs['role_arn'] if 'role_arn' in kwargs else None + self.targets = {} + + def enable(self): + self.state = 'ENABLED' + + def disable(self): + self.state = 'DISABLED' + + def put_targets(self, targets): + # TODO: Will need to test for valid ARNs. + for target in targets: + self.targets[target['TargetId']] = target + + def remove_targets(self, ids): + for target in ids: + if target in self.targets: + self.targets.pop(target) + + class EventsBackend(BaseBackend): def __init__(self): - self.events = {} - self.rules = {} + self.rules = OrderedDict() + self.next_tokens = {} - def can_paginate(self): - pass + def _gen_next_token(self, index): + token = binascii.hexlify(os.urandom(16)) + self.next_tokens[token] = index + return token - def delete_rule(self): - pass + def _process_token_and_limits(self, array_len, next_token=None, limit=None): + start_index = 0 + end_index = array_len + new_next_token = None + + if next_token is not None: + if next_token in self.next_tokens: + start_index = self.next_tokens[next_token] + + if limit is not None: + new_end_index = start_index + int(limit) + if new_end_index < end_index: + end_index = new_end_index + new_next_token = self._gen_next_token(end_index - 1) + + return start_index, end_index, new_next_token + + def delete_rule(self, name): + return self.rules.pop(name) is not None def describe_rule(self, name): - event = self.events['name'] + if name in self.rules: + return self.rules[name] - def disable_rule(self): - pass + return None - def enable_rule(self): - pass + def disable_rule(self, name): + if name in self.rules: + self.rules[name].disable() + return True + + return False + + def enable_rule(self, name): + if name in self.rules: + self.rules[name].enable() + return True + + return False def generate_presigned_url(self): pass - def get_paginator(self): - pass + def list_rule_names_by_target(self, target_arn, next_token=None, limit=None): + rules_array = self.rules.values() - def get_waiter(self): - pass + matching_rules = [] + return_obj = {} - def list_rule_names_by_target(self): - pass + start_index, end_index, new_next_token = self._process_token_and_limits(len(rules_array), next_token, limit) - def list_rules(self): - pass + for i in range(start_index, end_index): + rule = rules_array[i] + if target_arn in rule.targets: + matching_rules.append(rule.name) - def list_targets_by_rule(self): - pass + return_obj['RuleNames'] = matching_rules + if new_next_token is not None: + return_obj['NextToken'] = new_next_token + + return return_obj + + def list_rules(self, prefix=None, next_token=None, limit=None): + rules_array = self.rules.values() + + match_string = '.*' + if prefix is not None: + match_string = '^' + prefix + match_string + + match_regex = re.compile(match_string) + + matching_rules = [] + return_obj = {} + + start_index, end_index, new_next_token = self._process_token_and_limits(len(rules_array), next_token, limit) + + for i in range(start_index, end_index): + rule = rules_array[i] + if match_regex.match(rule.name): + matching_rules.append(rule) + + return_obj['Rules'] = matching_rules + if new_next_token is not None: + return_obj['NextToken'] = new_next_token + + return return_obj + + def list_targets_by_rule(self, rule, next_token=None, limit=None): + # We'll let a KeyError exception be thrown for response to handle if rule doesn't exist. + targets = self.rules[rule].targets.values() + + start_index, end_index, new_next_token = self._process_token_and_limits(len(targets), next_token, limit) + + returned_targets = [] + return_obj = {} + + for i in range(start_index, end_index): + returned_targets.append(targets[i]) + + return_obj['Targets'] = returned_targets + if new_next_token is not None: + return_obj['NextToken'] = new_next_token + + return return_obj def put_events(self): + # For the purposes of this mock, there is no backend action for putting an event. + # Response module will deal with replying. pass def put_rule(self, name, **kwargs): - pass + rule = Rule(name, **kwargs) + self.rules[rule.name] = rule + return rule.arn - def put_targets(self): - pass + def put_targets(self, name, targets): + self.rules[name].put_targets(targets) - def remove_targets(self): - pass + def remove_targets(self, name, ids): + self.rules[name].remove_targets(ids) def test_event_pattern(self): - pass + raise NotImplementedError() events_backend = EventsBackend() diff --git a/moto/events/responses.py b/moto/events/responses.py index 7be87d03..8099f5c5 100644 --- a/moto/events/responses.py +++ b/moto/events/responses.py @@ -5,8 +5,8 @@ from moto.core.responses import BaseResponse class EventsHandler(BaseResponse): - def error(self, type_, status=400): - return status, self.response_headers, json.dumps({'__type': type_}) + def error(self, type_, message='', status=400): + return status, self.response_headers, json.dumps({'__type': type_, 'message': message}) def can_paginate(self): pass @@ -46,7 +46,7 @@ class EventsHandler(BaseResponse): def put_rule(self): if 'Name' not in self.body: - return self.error("com.amazonaws.events.validate#ValidationException") + return self.error('ValidationException', 'Parameter Name is required.') pass def put_targets(self): From db0b494b4f5f9ddeb1c623ce4fcd3512fc488978 Mon Sep 17 00:00:00 2001 From: Justin Wiley Date: Thu, 1 Dec 2016 17:23:51 -0800 Subject: [PATCH 3/7] Completed the CloudWatch Events mocking module and tests. --- moto/backends.py | 9 +- moto/events/models.py | 52 +++++---- moto/events/responses.py | 180 +++++++++++++++++++++++++++---- moto/events/urls.py | 2 +- tests/test_events/test_events.py | 173 +++++++++++++++++++++++++++++ 5 files changed, 368 insertions(+), 48 deletions(-) create mode 100644 tests/test_events/test_events.py diff --git a/moto/backends.py b/moto/backends.py index d1262a7c..0cbcf481 100644 --- a/moto/backends.py +++ b/moto/backends.py @@ -1,28 +1,30 @@ from __future__ import unicode_literals + from moto.apigateway import apigateway_backend from moto.autoscaling import autoscaling_backend from moto.awslambda import lambda_backend -from moto.cloudwatch import cloudwatch_backend from moto.cloudformation import cloudformation_backend +from moto.cloudwatch import cloudwatch_backend from moto.datapipeline import datapipeline_backend from moto.dynamodb import dynamodb_backend from moto.dynamodb2 import dynamodb_backend2 from moto.ec2 import ec2_backend from moto.elb import elb_backend from moto.emr import emr_backend +from moto.events import events_backend from moto.glacier import glacier_backend from moto.iam import iam_backend -from moto.opsworks import opsworks_backend from moto.kinesis import kinesis_backend from moto.kms import kms_backend +from moto.opsworks import opsworks_backend from moto.rds import rds_backend from moto.redshift import redshift_backend +from moto.route53 import route53_backend from moto.s3 import s3_backend from moto.ses import ses_backend from moto.sns import sns_backend from moto.sqs import sqs_backend from moto.sts import sts_backend -from moto.route53 import route53_backend BACKENDS = { 'apigateway': apigateway_backend, @@ -34,6 +36,7 @@ BACKENDS = { 'dynamodb2': dynamodb_backend2, 'ec2': ec2_backend, 'elb': elb_backend, + 'events': events_backend, 'emr': emr_backend, 'glacier': glacier_backend, 'iam': iam_backend, diff --git a/moto/events/models.py b/moto/events/models.py index 5a10c165..6063a8b7 100644 --- a/moto/events/models.py +++ b/moto/events/models.py @@ -1,4 +1,3 @@ -import binascii import os import re from collections import OrderedDict @@ -13,12 +12,12 @@ class Rule(object): def __init__(self, name, **kwargs): self.name = name - self.arn = kwargs['arn'] if 'arn' in kwargs else self._generate_arn(name) - self.event_pattern = kwargs['event_pattern'] if 'event_pattern' in kwargs else None - self.schedule_exp = kwargs['schedule_exp'] if 'schedule_exp' in kwargs else None - self.state = kwargs['state'] if 'state' in kwargs else 'ENABLED' - self.description = kwargs['description'] if 'description' in kwargs else None - self.role_arn = kwargs['role_arn'] if 'role_arn' in kwargs else None + self.arn = kwargs.get('Arn') or self._generate_arn(name) + self.event_pattern = kwargs.get('EventPattern') + self.schedule_exp = kwargs.get('ScheduleExpression') + self.state = kwargs.get('State') or 'ENABLED' + self.description = kwargs.get('Description') + self.role_arn = kwargs.get('RoleArn') self.targets = {} def enable(self): @@ -28,9 +27,9 @@ class Rule(object): self.state = 'DISABLED' def put_targets(self, targets): - # TODO: Will need to test for valid ARNs. + # Not testing for valid ARNs. for target in targets: - self.targets[target['TargetId']] = target + self.targets[target['Id']] = target def remove_targets(self, ids): for target in ids: @@ -45,7 +44,7 @@ class EventsBackend(BaseBackend): self.next_tokens = {} def _gen_next_token(self, index): - token = binascii.hexlify(os.urandom(16)) + token = os.urandom(128).encode('base64') self.next_tokens[token] = index return token @@ -54,15 +53,14 @@ class EventsBackend(BaseBackend): end_index = array_len new_next_token = None - if next_token is not None: - if next_token in self.next_tokens: - start_index = self.next_tokens[next_token] + if next_token: + start_index = self.next_tokens.pop(next_token, 0) if limit is not None: new_end_index = start_index + int(limit) if new_end_index < end_index: end_index = new_end_index - new_next_token = self._gen_next_token(end_index - 1) + new_next_token = self._gen_next_token(end_index) return start_index, end_index, new_next_token @@ -70,10 +68,7 @@ class EventsBackend(BaseBackend): return self.rules.pop(name) is not None def describe_rule(self, name): - if name in self.rules: - return self.rules[name] - - return None + return self.rules.get(name) def disable_rule(self, name): if name in self.rules: @@ -102,8 +97,9 @@ class EventsBackend(BaseBackend): for i in range(start_index, end_index): rule = rules_array[i] - if target_arn in rule.targets: - matching_rules.append(rule.name) + for target in rule.targets: + if rule.targets[target]['Arn'] == target_arn: + matching_rules.append(rule.name) return_obj['RuleNames'] = matching_rules if new_next_token is not None: @@ -165,10 +161,22 @@ class EventsBackend(BaseBackend): return rule.arn def put_targets(self, name, targets): - self.rules[name].put_targets(targets) + rule = self.rules.get(name) + + if rule: + rule.put_targets(targets) + return True + + return False def remove_targets(self, name, ids): - self.rules[name].remove_targets(ids) + rule = self.rules.get(name) + + if rule: + rule.remove_targets(ids) + return True + + return False def test_event_pattern(self): raise NotImplementedError() diff --git a/moto/events/responses.py b/moto/events/responses.py index 8099f5c5..7d63388b 100644 --- a/moto/events/responses.py +++ b/moto/events/responses.py @@ -1,59 +1,195 @@ import json +import re from moto.core.responses import BaseResponse +from moto.events import events_backend class EventsHandler(BaseResponse): - def error(self, type_, message='', status=400): - return status, self.response_headers, json.dumps({'__type': type_, 'message': message}) + def _generate_rule_dict(self, rule): + return { + 'Name': rule.name, + 'Arn': rule.arn, + 'EventPattern': rule.event_pattern, + 'State': rule.state, + 'Description': rule.description, + 'ScheduleExpression': rule.schedule_exp, + 'RoleArn': rule.role_arn + } - def can_paginate(self): - pass + def load_body(self): + decoded_body = self.body.decode('utf-8') + return json.loads(decoded_body or '{}') + + def error(self, type_, message='', status=400): + headers = self.response_headers + headers['status'] = status + return json.dumps({'__type': type_, 'message': message}), headers, def delete_rule(self): - pass + body = self.load_body() + name = body.get('NamePrefix') + + if not name: + return self.error('ValidationException', 'Parameter Name is required.') + + return '', self.response_headers def describe_rule(self): - pass + body = self.load_body() + name = body.get('Name') + + if not name: + return self.error('ValidationException', 'Parameter Name is required.') + + rule = events_backend.describe_rule(name) + + if not rule: + return self.error('ResourceNotFoundException', 'Rule test does not exist.') + + rule_dict = self._generate_rule_dict(rule) + return json.dumps(rule_dict), self.response_headers def disable_rule(self): - pass + body = self.load_body() + name = body.get('Name') + + if not name: + return self.error('ValidationException', 'Parameter Name is required.') + + if not events_backend.disable_rule(name): + return self.error('ResourceNotFoundException', 'Rule ' + name + ' does not exist.') + + return '', self.response_headers def enable_rule(self): - pass + body = self.load_body() + name = body.get('Name') + + if not name: + return self.error('ValidationException', 'Parameter Name is required.') + + if not events_backend.enable_rule(name): + return self.error('ResourceNotFoundException', 'Rule ' + name + ' does not exist.') + + return '', self.response_headers def generate_presigned_url(self): pass - def get_paginator(self): - pass - - def get_waiter(self): - pass - def list_rule_names_by_target(self): - pass + body = self.load_body() + target_arn = body.get('TargetArn') + next_token = body.get('NextToken') + limit = body.get('Limit') + + if not target_arn: + return self.error('ValidationException', 'Parameter TargetArn is required.') + + rule_names = events_backend.list_rule_names_by_target(target_arn, next_token, limit) + + return json.dumps(rule_names), self.response_headers def list_rules(self): - pass + body = self.load_body() + prefix = body.get('NamePrefix') + next_token = body.get('NextToken') + limit = body.get('Limit') + + rules = events_backend.list_rules(prefix, next_token, limit) + rules_obj = {'Rules': []} + + for rule in rules['Rules']: + rules_obj['Rules'].append(self._generate_rule_dict(rule)) + + if rules.get('NextToken'): + rules_obj['NextToken'] = rules['NextToken'] + + return json.dumps(rules_obj), self.response_headers def list_targets_by_rule(self): - pass + body = self.load_body() + rule_name = body.get('Rule') + next_token = body.get('NextToken') + limit = body.get('Limit') + + if not rule_name: + return self.error('ValidationException', 'Parameter Rule is required.') + + try: + targets = events_backend.list_targets_by_rule(rule_name, next_token, limit) + except KeyError: + return self.error('ResourceNotFoundException', 'Rule ' + rule_name + ' does not exist.') + + return json.dumps(targets), self.response_headers def put_events(self): - pass + return '', self.response_headers def put_rule(self): - if 'Name' not in self.body: + body = self.load_body() + name = body.get('Name') + event_pattern = body.get('EventPattern') + sched_exp = body.get('ScheduleExpression') + + if not name: return self.error('ValidationException', 'Parameter Name is required.') - pass + + if event_pattern: + try: + json.loads(event_pattern) + except ValueError: + # Not quite as informative as the real error, but it'll work for now. + return self.error('InvalidEventPatternException', 'Event pattern is not valid.') + + if sched_exp: + if not (re.match('^cron\(.*\)', sched_exp) or + re.match('^rate\(\d*\s(minute|minutes|hour|hours|day|days)\)', sched_exp)): + return self.error('ValidationException', 'Parameter ScheduleExpression is not valid.') + + rule_arn = events_backend.put_rule( + name, + ScheduleExpression=sched_exp, + EventPattern=event_pattern, + State=body.get('State'), + Description=body.get('Description'), + RoleArn=body.get('RoleArn') + ) + + return json.dumps({'RuleArn': rule_arn}), self.response_headers def put_targets(self): - pass + body = self.load_body() + rule_name = body.get('Rule') + targets = body.get('Targets') + + if not rule_name: + return self.error('ValidationException', 'Parameter Rule is required.') + + if not targets: + return self.error('ValidationException', 'Parameter Targets is required.') + + if not events_backend.put_targets(rule_name, targets): + return self.error('ResourceNotFoundException', 'Rule ' + rule_name + ' does not exist.') + + return '', self.response_headers def remove_targets(self): - pass + body = self.load_body() + rule_name = body.get('Rule') + ids = body.get('Ids') + + if not rule_name: + return self.error('ValidationException', 'Parameter Rule is required.') + + if not ids: + return self.error('ValidationException', 'Parameter Ids is required.') + + if not events_backend.remove_targets(rule_name, ids): + return self.error('ResourceNotFoundException', 'Rule ' + rule_name + ' does not exist.') + + return '', self.response_headers def test_event_pattern(self): pass diff --git a/moto/events/urls.py b/moto/events/urls.py index 9484e385..c1ad554f 100644 --- a/moto/events/urls.py +++ b/moto/events/urls.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals from .responses import EventsHandler url_bases = [ - "https?://events.(.+).amazonaws.com" + "https://events.(.+).amazonaws.com" ] url_paths = { diff --git a/tests/test_events/test_events.py b/tests/test_events/test_events.py new file mode 100644 index 00000000..96d55bd0 --- /dev/null +++ b/tests/test_events/test_events.py @@ -0,0 +1,173 @@ +import random + +import boto3 + +from moto.events import mock_events + + +RULES = [ + {'Name': 'test1', 'ScheduleExpression': 'rate(5 minutes)'}, + {'Name': 'test2', 'ScheduleExpression': 'rate(1 minute)'}, + {'Name': 'test3', 'EventPattern': '{"source": ["test-source"]}'} +] + +TARGETS = { + 'test-target-1': { + 'Id': 'test-target-1', + 'Arn': 'arn:aws:lambda:us-west-2:111111111111:function:test-function-1', + 'Rules': ['test1', 'test2'] + }, + 'test-target-2': { + 'Id': 'test-target-2', + 'Arn': 'arn:aws:lambda:us-west-2:111111111111:function:test-function-2', + 'Rules': ['test1', 'test3'] + }, + 'test-target-3': { + 'Id': 'test-target-3', + 'Arn': 'arn:aws:lambda:us-west-2:111111111111:function:test-function-3', + 'Rules': ['test1', 'test2'] + }, + 'test-target-4': { + 'Id': 'test-target-4', + 'Arn': 'arn:aws:lambda:us-west-2:111111111111:function:test-function-4', + 'Rules': ['test1', 'test3'] + }, + 'test-target-5': { + 'Id': 'test-target-5', + 'Arn': 'arn:aws:lambda:us-west-2:111111111111:function:test-function-5', + 'Rules': ['test1', 'test2'] + }, + 'test-target-6': { + 'Id': 'test-target-6', + 'Arn': 'arn:aws:lambda:us-west-2:111111111111:function:test-function-6', + 'Rules': ['test1', 'test3'] + } +} + + +def get_random_rule(): + return RULES[random.randint(0, len(RULES) - 1)] + + +@mock_events +def generate_environment(): + client = boto3.client('events', 'us-west-2') + + for rule in RULES: + client.put_rule( + Name=rule['Name'], + ScheduleExpression=rule.get('ScheduleExpression', ''), + EventPattern=rule.get('EventPattern', '') + ) + + targets = [] + for target, target_attr in TARGETS.iteritems(): + if rule['Name'] in target_attr.get('Rules'): + targets.append({'Id': target, 'Arn': target_attr['Arn']}) + + client.put_targets(Rule=rule['Name'], Targets=targets) + + return client + + +@mock_events +def test_list_rules(): + client = generate_environment() + response = client.list_rules() + + assert(response is not None) + assert(len(response['Rules']) > 0) + + +@mock_events +def test_describe_rule(): + rule_name = get_random_rule()['Name'] + client = generate_environment() + response = client.describe_rule(Name=rule_name) + + assert(response is not None) + assert(response.get('Name') == rule_name) + assert(response.get('Arn') is not None) + + +@mock_events +def test_enable_disable_rule(): + rule_name = get_random_rule()['Name'] + client = generate_environment() + + # Rules should start out enabled in these tests. + rule = client.describe_rule(Name=rule_name) + assert(rule['State'] == 'ENABLED') + + client.disable_rule(Name=rule_name) + rule = client.describe_rule(Name=rule_name) + assert(rule['State'] == 'DISABLED') + + client.enable_rule(Name=rule_name) + rule = client.describe_rule(Name=rule_name) + assert(rule['State'] == 'ENABLED') + + +@mock_events +def test_list_rule_names_by_target(): + test_1_target = TARGETS['test-target-1'] + test_2_target = TARGETS['test-target-2'] + client = generate_environment() + + rules = client.list_rule_names_by_target(TargetArn=test_1_target['Arn']) + assert(len(rules) == len(test_1_target['Rules'])) + for rule in rules['RuleNames']: + assert(rule in test_1_target['Rules']) + + rules = client.list_rule_names_by_target(TargetArn=test_2_target['Arn']) + assert(len(rules) == len(test_2_target['Rules'])) + for rule in rules['RuleNames']: + assert(rule in test_2_target['Rules']) + + +@mock_events +def test_list_rules(): + client = generate_environment() + + rules = client.list_rules() + assert(len(rules['Rules']) == len(RULES)) + + +@mock_events +def test_list_targets_by_rule(): + rule_name = get_random_rule()['Name'] + client = generate_environment() + targets = client.list_targets_by_rule(Rule=rule_name) + + expected_targets = [] + for target, attrs in TARGETS.iteritems(): + if rule_name in attrs.get('Rules'): + expected_targets.append(target) + + assert(len(targets['Targets']) == len(expected_targets)) + + +@mock_events +def test_remove_targets(): + rule_name = get_random_rule()['Name'] + client = generate_environment() + + targets = client.list_targets_by_rule(Rule=rule_name)['Targets'] + targets_before = len(targets) + assert(targets_before > 0) + + client.remove_targets(Rule=rule_name, Ids=[targets[0]['Id']]) + + targets = client.list_targets_by_rule(Rule=rule_name)['Targets'] + targets_after = len(targets) + assert(targets_before - 1 == targets_after) + + +if __name__ == '__main__': + test_list_rules() + test_describe_rule() + test_enable_disable_rule() + test_list_rule_names_by_target() + test_list_rules() + test_list_targets_by_rule() + test_remove_targets() From 6c85a85e0d90c5e7ad0fdc1f7723c9fe2548466f Mon Sep 17 00:00:00 2001 From: Justin Wiley Date: Thu, 1 Dec 2016 19:10:59 -0800 Subject: [PATCH 4/7] Removed OrderedDicts for 2.6 and dict.iteritems() calls for 3.3+ compatibility. --- moto/events/models.py | 29 ++++++++++++----------------- tests/test_events/test_events.py | 10 +++++----- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/moto/events/models.py b/moto/events/models.py index 6063a8b7..12ee7ef0 100644 --- a/moto/events/models.py +++ b/moto/events/models.py @@ -40,9 +40,14 @@ class Rule(object): class EventsBackend(BaseBackend): def __init__(self): - self.rules = OrderedDict() + self.rules = {} + # This array tracks the order in which the rules have been added, since 2.6 doesn't have OrderedDicts + self.rules_order = [] self.next_tokens = {} + def _get_rule_by_index(self, i): + return self.rules.get(self.rules_order[i]) + def _gen_next_token(self, index): token = os.urandom(128).encode('base64') self.next_tokens[token] = index @@ -65,6 +70,7 @@ class EventsBackend(BaseBackend): return start_index, end_index, new_next_token def delete_rule(self, name): + self.rules_order.pop(self.rules_order.index(name)) return self.rules.pop(name) is not None def describe_rule(self, name): @@ -84,19 +90,14 @@ class EventsBackend(BaseBackend): return False - def generate_presigned_url(self): - pass - def list_rule_names_by_target(self, target_arn, next_token=None, limit=None): - rules_array = self.rules.values() - matching_rules = [] return_obj = {} - start_index, end_index, new_next_token = self._process_token_and_limits(len(rules_array), next_token, limit) + start_index, end_index, new_next_token = self._process_token_and_limits(len(self.rules), next_token, limit) for i in range(start_index, end_index): - rule = rules_array[i] + rule = self._get_rule_by_index(i) for target in rule.targets: if rule.targets[target]['Arn'] == target_arn: matching_rules.append(rule.name) @@ -108,8 +109,6 @@ class EventsBackend(BaseBackend): return return_obj def list_rules(self, prefix=None, next_token=None, limit=None): - rules_array = self.rules.values() - match_string = '.*' if prefix is not None: match_string = '^' + prefix + match_string @@ -119,10 +118,10 @@ class EventsBackend(BaseBackend): matching_rules = [] return_obj = {} - start_index, end_index, new_next_token = self._process_token_and_limits(len(rules_array), next_token, limit) + start_index, end_index, new_next_token = self._process_token_and_limits(len(self.rules), next_token, limit) for i in range(start_index, end_index): - rule = rules_array[i] + rule = self._get_rule_by_index(i) if match_regex.match(rule.name): matching_rules.append(rule) @@ -150,14 +149,10 @@ class EventsBackend(BaseBackend): return return_obj - def put_events(self): - # For the purposes of this mock, there is no backend action for putting an event. - # Response module will deal with replying. - pass - def put_rule(self, name, **kwargs): rule = Rule(name, **kwargs) self.rules[rule.name] = rule + self.rules_order.append(rule.name) return rule.arn def put_targets(self, name, targets): diff --git a/tests/test_events/test_events.py b/tests/test_events/test_events.py index 96d55bd0..a2d5a5d4 100644 --- a/tests/test_events/test_events.py +++ b/tests/test_events/test_events.py @@ -61,9 +61,9 @@ def generate_environment(): ) targets = [] - for target, target_attr in TARGETS.iteritems(): - if rule['Name'] in target_attr.get('Rules'): - targets.append({'Id': target, 'Arn': target_attr['Arn']}) + for target in TARGETS: + if rule['Name'] in TARGETS[target].get('Rules'): + targets.append({'Id': target, 'Arn': TARGETS[target]['Arn']}) client.put_targets(Rule=rule['Name'], Targets=targets) @@ -140,8 +140,8 @@ def test_list_targets_by_rule(): targets = client.list_targets_by_rule(Rule=rule_name) expected_targets = [] - for target, attrs in TARGETS.iteritems(): - if rule_name in attrs.get('Rules'): + for target in TARGETS: + if rule_name in TARGETS[target].get('Rules'): expected_targets.append(target) assert(len(targets['Targets']) == len(expected_targets)) From 5d7a102e4c453a2b4eb64cb4ad617798017b9605 Mon Sep 17 00:00:00 2001 From: Justin Wiley Date: Thu, 1 Dec 2016 19:24:40 -0800 Subject: [PATCH 5/7] Stoopid import was still hanging around. --- moto/events/models.py | 1 - moto/events/urls.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/moto/events/models.py b/moto/events/models.py index 12ee7ef0..83386cf8 100644 --- a/moto/events/models.py +++ b/moto/events/models.py @@ -1,6 +1,5 @@ import os import re -from collections import OrderedDict from moto.core import BaseBackend diff --git a/moto/events/urls.py b/moto/events/urls.py index c1ad554f..bff05da3 100644 --- a/moto/events/urls.py +++ b/moto/events/urls.py @@ -8,4 +8,4 @@ url_bases = [ url_paths = { "{0}/": EventsHandler.dispatch, -} \ No newline at end of file +} From c7757f953cbc90633fd0f54518c2bb70f9142cbc Mon Sep 17 00:00:00 2001 From: Justin Wiley Date: Thu, 1 Dec 2016 19:52:00 -0800 Subject: [PATCH 6/7] Can't iterate over dict values in Python 3.3+. Changed Rule.targets from a dict to an array, which is probably better anyway since the dict doesn't maintain order, making API calls with the Limit parameter specified unreliable. --- moto/events/models.py | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/moto/events/models.py b/moto/events/models.py index 83386cf8..94cca5ee 100644 --- a/moto/events/models.py +++ b/moto/events/models.py @@ -17,7 +17,7 @@ class Rule(object): self.state = kwargs.get('State') or 'ENABLED' self.description = kwargs.get('Description') self.role_arn = kwargs.get('RoleArn') - self.targets = {} + self.targets = [] def enable(self): self.state = 'ENABLED' @@ -25,22 +25,35 @@ class Rule(object): def disable(self): self.state = 'DISABLED' + # This song and dance for targets is because we need order for Limits and NextTokens, but can't use OrderedDicts + # with Python 2.6, so tracking it with an array it is. + def _check_target_exists(self, target_id): + for i in range(0, len(self.targets)): + if target_id == self.targets[i]['Id']: + return i + return None + def put_targets(self, targets): # Not testing for valid ARNs. for target in targets: - self.targets[target['Id']] = target + index = self._check_target_exists(target['Id']) + if index is not None: + self.targets[index] = target + else: + self.targets.append(target) def remove_targets(self, ids): - for target in ids: - if target in self.targets: - self.targets.pop(target) + for target_id in ids: + index = self._check_target_exists(target_id) + if index is not None: + self.targets.pop(index) class EventsBackend(BaseBackend): def __init__(self): self.rules = {} - # This array tracks the order in which the rules have been added, since 2.6 doesn't have OrderedDicts + # This array tracks the order in which the rules have been added, since 2.6 doesn't have OrderedDicts. self.rules_order = [] self.next_tokens = {} @@ -98,7 +111,7 @@ class EventsBackend(BaseBackend): for i in range(start_index, end_index): rule = self._get_rule_by_index(i) for target in rule.targets: - if rule.targets[target]['Arn'] == target_arn: + if target['Arn'] == target_arn: matching_rules.append(rule.name) return_obj['RuleNames'] = matching_rules @@ -132,15 +145,15 @@ class EventsBackend(BaseBackend): def list_targets_by_rule(self, rule, next_token=None, limit=None): # We'll let a KeyError exception be thrown for response to handle if rule doesn't exist. - targets = self.rules[rule].targets.values() + rule = self.rules[rule] - start_index, end_index, new_next_token = self._process_token_and_limits(len(targets), next_token, limit) + start_index, end_index, new_next_token = self._process_token_and_limits(len(rule.targets), next_token, limit) returned_targets = [] return_obj = {} for i in range(start_index, end_index): - returned_targets.append(targets[i]) + returned_targets.append(rule.targets[i]) return_obj['Targets'] = returned_targets if new_next_token is not None: From 24fdf5b6fec8c431d564e9fb4b1bf68ddc1b913b Mon Sep 17 00:00:00 2001 From: Justin Wiley Date: Thu, 1 Dec 2016 20:02:54 -0800 Subject: [PATCH 7/7] Added myself as a contributor. :P --- AUTHORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.md b/AUTHORS.md index be500fae..356f8826 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -42,3 +42,4 @@ Moto is written by Steve Pulec with contributions from: * [Pior Bastida](https://github.com/pior) * [Dustin J. Mitchell](https://github.com/djmitche) * [Jean-Baptiste Barth](https://github.com/jbbarth) +* [Justin Wiley](https://github.com/SectorNine50)