class TimestampSequencedItemTestCase(RecordManagerTestCase): EXAMPLE_EVENT_TOPIC1 = get_topic(TimestampedEventExample1) EXAMPLE_EVENT_TOPIC2 = get_topic(TimestampedEventExample2) def construct_positions(self, num=3): while num: yield decimaltimestamp() num -= 1 sleep(0.00001) def construct_record_manager(self): return self.factory.construct_timestamp_sequenced_record_manager()
def test_hash(self): entity_id1 = uuid4() event1 = Example.Created(originator_id=entity_id1, originator_topic=get_topic(Example), a=1, b=2, timestamp=3) event2 = Example.Created(originator_id=entity_id1, originator_topic=get_topic(Example), a=1, b=2, timestamp=3) self.assertEqual(hash(event1), hash(event2))
class IntegerSequencedRecordTestCase(RecordManagerTestCase): """ Abstract test case for integer sequenced record managers. """ EXAMPLE_EVENT_TOPIC1 = get_topic(VersionedEventExample1) EXAMPLE_EVENT_TOPIC2 = get_topic(VersionedEventExample2) def construct_positions(self): return 0, 1, 2 def construct_record_manager(self): return self.factory.construct_integer_sequenced_record_manager()
def test_example_aggregate_event_classes(self): self.assertIn("Event", ExampleAggregateRoot.__dict__) self.assertIn("Created", ExampleAggregateRoot.__dict__) self.assertIn("Discarded", ExampleAggregateRoot.__dict__) self.assertIn("AttributeChanged", ExampleAggregateRoot.__dict__) self.assertEqual(ExampleAggregateRoot.Event.__name__, "Event") self.assertEqual(ExampleAggregateRoot.Event.__qualname__, "ExampleAggregateRoot.Event") topic = "eventsourcing.tests.core_tests.test_aggregate_root#ExampleAggregateRoot.Event" self.assertEqual(get_topic(ExampleAggregateRoot.Event), topic) self.assertEqual(resolve_topic(topic), ExampleAggregateRoot.Event) self.assertEqual(ExampleAggregateRoot.Created.__name__, "Created") self.assertEqual(ExampleAggregateRoot.Created.__qualname__, "ExampleAggregateRoot.Created") topic = "eventsourcing.tests.core_tests.test_aggregate_root#ExampleAggregateRoot.Created" self.assertEqual(get_topic(ExampleAggregateRoot.Created), topic) self.assertEqual(resolve_topic(topic), ExampleAggregateRoot.Created) self.assertTrue( issubclass(ExampleAggregateRoot.Created, ExampleAggregateRoot.Event)) self.assertEqual(ExampleAggregateRoot.Discarded.__name__, "Discarded") self.assertEqual( ExampleAggregateRoot.Discarded.__qualname__, "ExampleAggregateRoot.Discarded", ) topic = "eventsourcing.tests.core_tests.test_aggregate_root#ExampleAggregateRoot.Discarded" self.assertEqual(get_topic(ExampleAggregateRoot.Discarded), topic) self.assertEqual(resolve_topic(topic), ExampleAggregateRoot.Discarded) self.assertTrue( issubclass(ExampleAggregateRoot.Discarded, ExampleAggregateRoot.Event)) self.assertEqual(ExampleAggregateRoot.ExampleCreated.__name__, "ExampleCreated") self.assertEqual( ExampleAggregateRoot.ExampleCreated.__qualname__, "ExampleAggregateRoot.ExampleCreated", ) topic = "eventsourcing.tests.core_tests.test_aggregate_root#ExampleAggregateRoot.ExampleCreated" self.assertEqual(get_topic(ExampleAggregateRoot.ExampleCreated), topic) self.assertEqual(resolve_topic(topic), ExampleAggregateRoot.ExampleCreated) self.assertTrue( issubclass(ExampleAggregateRoot.ExampleCreated, ExampleAggregateRoot.Event))
def construct_item_args(self, domain_event): """ Constructs attributes of a sequenced item from the given domain event. """ # Copy the state of the event. event_attrs = domain_event.__dict__.copy() # Get the sequence ID. sequence_id = event_attrs.get(self.sequence_id_attr_name) # Get the position in the sequence. position = event_attrs.get(self.position_attr_name) # Get the topic from the event attrs, otherwise from the class. topic = get_topic(domain_event.__class__) # Serialise the remaining event attribute values. data = json_dumps(event_attrs, cls=self.json_encoder_class) # Encrypt data. if self.cipher: data = self.cipher.encrypt(data) # Get the 'other' args. # - these are meant to be derivative of the other attributes, # to populate database fields, and shouldn't affect the hash. other_args = tuple((getattr(domain_event, name) for name in self.other_attr_names)) return (sequence_id, position, topic, data) + other_args
def test_with_versioned_entity_event(self): # Setup the mapper, and create an event. mapper = SequencedItemMapper(sequenced_item_class=SequencedItem, sequence_id_attr_name='originator_id', position_attr_name='originator_version') entity_id1 = uuid4() event1 = Event1(originator_id=entity_id1, originator_version=101) # Check to_sequenced_item() method results in a sequenced item. sequenced_item = mapper.item_from_event(event1) self.assertIsInstance(sequenced_item, SequencedItem) self.assertEqual(sequenced_item.position, 101) self.assertEqual(sequenced_item.sequence_id, entity_id1) self.assertEqual(sequenced_item.topic, get_topic(Event1)) self.assertTrue(sequenced_item.state) # Use the returned values to create a new sequenced item. sequenced_item_copy = SequencedItem( sequence_id=sequenced_item.sequence_id, position=sequenced_item.position, topic=sequenced_item.topic, state=sequenced_item.state, ) # Check from_sequenced_item() returns an event. domain_event = mapper.event_from_item(sequenced_item_copy) self.assertIsInstance(domain_event, Event1) self.assertEqual(domain_event.originator_id, event1.originator_id) self.assertEqual(domain_event.originator_version, event1.originator_version)
def test_get_item(self): # Setup an event store. event_store = self.construct_event_store() # Put an event in the event store. entity_id = uuid4() event_store.store( Example.Created( a=1, b=2, originator_id=entity_id, originator_topic=get_topic(Example), )) # Construct a repository. event_sourced_repo = EventSourcedRepository(event_store=event_store) # Check the entity attributes. example = event_sourced_repo[entity_id] self.assertEqual(1, example.a) self.assertEqual(2, example.b) self.assertEqual(entity_id, example.id) # Setup an example repository, using the subclass ExampleRepository. example_repo = ExampleRepository(event_store=event_store) # Check the repo has the example. self.assertIn(entity_id, example_repo) self.assertNotIn(uuid4(), example_repo) # Check the entity attributes. example = example_repo[entity_id] self.assertEqual(1, example.a) self.assertEqual(2, example.b) self.assertEqual(entity_id, example.id)
def default(self, obj): if isinstance(obj, UUID): return {'UUID': obj.hex} elif isinstance(obj, datetime.datetime): return {'ISO8601_datetime': obj.strftime('%Y-%m-%dT%H:%M:%S.%f%z')} elif isinstance(obj, datetime.date): return {'ISO8601_date': obj.isoformat()} elif isinstance(obj, datetime.time): return {'ISO8601_time': obj.strftime('%H:%M:%S.%f')} elif isinstance(obj, Decimal): return { '__decimal__': str(obj), } elif hasattr(obj, '__class__') and hasattr(obj, '__dict__'): topic = get_topic(obj.__class__) state = obj.__dict__.copy() return { '__class__': { 'topic': topic, 'state': state, } } elif isinstance(obj, deque): assert list(obj) == [] return {'__deque__': []} # Let the base class default method raise the TypeError. return JSONEncoder.default(self, obj)
def test_with_timestamped_entity_event(self): # Setup the mapper, and create an event. mapper = SequencedItemMapper( sequenced_item_class=SequencedItem, sequence_id_attr_name="originator_id", position_attr_name="timestamp", ) before = decimaltimestamp() sleep(0.000001) # Avoid test failing due to timestamp having limited precision. event2 = Event2(originator_id="entity2") sleep(0.000001) # Avoid test failing due to timestamp having limited precision. after = decimaltimestamp() # Check to_sequenced_item() method results in a sequenced item. sequenced_item = mapper.item_from_event(event2) self.assertIsInstance(sequenced_item, SequencedItem) self.assertGreater(sequenced_item.position, before) self.assertLess(sequenced_item.position, after) self.assertEqual(sequenced_item.sequence_id, "entity2") self.assertEqual(sequenced_item.topic, get_topic(Event2)) self.assertTrue(sequenced_item.state) # Use the returned values to create a new sequenced item. sequenced_item_copy = SequencedItem( sequence_id=sequenced_item.sequence_id, position=sequenced_item.position, topic=sequenced_item.topic, state=sequenced_item.state, ) # Check from_sequenced_item() returns an event. domain_event = mapper.event_from_item(sequenced_item_copy) self.assertIsInstance(domain_event, Event2) self.assertEqual(domain_event.originator_id, event2.originator_id) self.assertEqual(domain_event.timestamp, event2.timestamp)
def append_notification(self, item): sequenced_item = self.entity_record_manager.sequenced_item_class( uuid4(), 0, get_topic(DomainEvent), item ) self.entity_record_manager.append(sequenced_item)
def __create__(cls, originator_id=None, event_class=None, **kwargs): if originator_id is None: originator_id = uuid4() event = (event_class or cls.Created)(originator_id=originator_id, originator_topic=get_topic(cls), **kwargs) obj = event.__mutate__() obj.__publish__(event) return obj
def __init__(self, **kwargs): super(EventWithHash, self).__init__(**kwargs) # Set __event_topic__ to differentiate events of # different types with otherwise equal attributes. self.__dict__['__event_topic__'] = get_topic(type(self)) # Set __event_hash__ with a SHA-256 hash of the event. self.__dict__['__event_hash__'] = self.__hash_object__(self.__dict__)
def __hash__(self): """ Computes a Python integer hash for an event, using its event hash string. Supports equality and inequality comparisons. """ state = self.__dict__.copy() state['__event_topic__'] = get_topic(type(self)) return hash(self.__hash_object__(state))
def iterencode(self, o, _one_shot=False): if isinstance(o, tuple): o = { "__tuple__": { "topic": (get_topic(o.__class__)), "state": (list(o)) } } return super(ObjectJSONEncoder, self).iterencode(o, _one_shot=_one_shot)
def encode_deque(self, o): if type(o) == deque: return {"__deque__": self.encode_object(list(o))} else: return { "__deque__": { "state": self.encode_object(list(o)), "topic": get_topic(o.__class__), } }
def encode_list(self, o): if type(o) == list: return [self.encode_object(i) for i in o] else: return { "__list__": { "state": [self.encode_object(i) for i in o], "topic": get_topic(o.__class__), } }
def encode_frozenset(self, o): if type(o) == frozenset: return {"__frozenset__": self.encode_iterable(o)} else: return { "__frozenset__": { "state": self.encode_iterable(o), "topic": get_topic(o.__class__), } }
def encode_dict(self, o): if type(o) == dict: return self.encode_dict_state(o) else: return { "__dict__": { "topic": get_topic(o.__class__), "state": self.encode_dict_state(o), } }
def __init__(self, **kwargs: Any): super(EventWithHash, self).__init__(**kwargs) # Set __event_topic__ to differentiate events of # different types with otherwise equal attributes. self.__dict__["__event_topic__"] = get_topic(type(self)) # Set __event_hash__ with a SHA-256 hash of the event. hash_method = self.__hash_object_v2__ self.__dict__["__event_hash_method_name__"] = hash_method.__name__ self.__dict__["__event_hash__"] = hash_method(self.__dict__)
def test_event_hash_error(self): event1 = AggregateRoot.Created( originator_version=0, originator_id='1', originator_topic=get_topic(AggregateRoot)) event1.__check_hash__() # Break the hash. event1.__dict__['event_hash'] = 'damage' with self.assertRaises(EventHashError): event1.__check_hash__()
def test_aggregate1_event_classes(self): self.assertIn("Event", Aggregate1.__dict__) self.assertIn("Created", Aggregate1.__dict__) self.assertIn("Discarded", Aggregate1.__dict__) self.assertIn("AttributeChanged", Aggregate1.__dict__) self.assertEqual(Aggregate1.Event.__name__, "Event") self.assertEqual(Aggregate1.Event.__qualname__, "Aggregate1.Event") topic = "eventsourcing.tests.core_tests.test_aggregate_root#Aggregate1.Event" self.assertEqual(get_topic(Aggregate1.Event), topic) self.assertEqual(resolve_topic(topic), Aggregate1.Event) self.assertEqual(Aggregate1.Created.__name__, "Created") self.assertEqual(Aggregate1.Created.__qualname__, "Aggregate1.Created") topic = "eventsourcing.tests.core_tests.test_aggregate_root#Aggregate1.Created" self.assertEqual(get_topic(Aggregate1.Created), topic) self.assertEqual(resolve_topic(topic), Aggregate1.Created) self.assertTrue(issubclass(Aggregate1.Created, Aggregate1.Event)) self.assertEqual(Aggregate1.Discarded.__name__, "Discarded") self.assertEqual(Aggregate1.Discarded.__qualname__, "Aggregate1.Discarded") topic = ( "eventsourcing.tests.core_tests.test_aggregate_root#Aggregate1" ".Discarded") self.assertEqual(get_topic(Aggregate1.Discarded), topic) self.assertEqual(resolve_topic(topic), Aggregate1.Discarded) self.assertTrue(issubclass(Aggregate1.Discarded, Aggregate1.Event)) self.assertEqual(Aggregate1.ExampleCreated.__name__, "ExampleCreated") self.assertEqual(Aggregate1.ExampleCreated.__qualname__, "Aggregate1.ExampleCreated") topic = ( "eventsourcing.tests.core_tests.test_aggregate_root#Aggregate1" ".ExampleCreated") self.assertEqual(get_topic(Aggregate1.ExampleCreated), topic) self.assertEqual(resolve_topic(topic), Aggregate1.ExampleCreated) self.assertTrue(issubclass(Aggregate1.ExampleCreated, Aggregate1.Event)) self.assertTrue( issubclass(Aggregate1.SomethingElseOccurred, Aggregate1.Event))
def __create__(cls, originator_id=None, event_class=None, **kwargs): if originator_id is None: originator_id = uuid4() if getattr(cls, '__with_data_integrity__', True): genesis_hash = getattr(cls, '__genesis_hash__', GENESIS_HASH) kwargs['__previous_hash__'] = genesis_hash event = (event_class or cls.Created)(originator_id=originator_id, originator_topic=get_topic(cls), **kwargs) obj = event.__mutate__() obj.__publish__(event) return obj
def __init__(self, **kwargs): """ Initialises event attribute values directly from constructor kwargs. """ if self.__with_data_integrity__: kwargs['__event_topic__'] = get_topic(type(self)) # Seal the event with a hash of the other values. assert '__event_hash__' not in kwargs event_hash = self.__hash_for_data_integrity__(kwargs) kwargs['__event_hash__'] = event_hash self.__dict__.update(kwargs)
def test_equality_comparison(self): entity_id1 = uuid4() event1 = Example.Created(originator_id=entity_id1, originator_topic=get_topic(Example), a=1, b=2, timestamp=3) event2 = Example.Created(originator_id=entity_id1, originator_topic=get_topic(Example), a=1, b=2, timestamp=3) event3 = Example.Created(originator_id=entity_id1, originator_topic=get_topic(Example), a=3, b=2, timestamp=3) self.assertEqual(event1, event2) self.assertNotEqual(event1, event3) self.assertNotEqual(event2, event3) self.assertNotEqual(event2, None)
def __init__(self, **kwargs): """ Initialises event attribute values directly from constructor kwargs. """ super(EventWithHash, self).__init__(**kwargs) # Seal the event with a hash of the other values. # __event_topic__ is needed to obtain a different hash # for different types of events with otherwise equal # attributes. self.__dict__['__event_topic__'] = get_topic(type(self)) self.__dict__['__event_hash__'] = self.__hash_object__(self.__dict__)
def get_item_topic_and_state(self, domain_event_class, event_attrs): # Get the topic from the event attrs, otherwise from the class. topic = get_topic(domain_event_class) # Serialise the event attributes. state = json_dumps(event_attrs, cls=self.json_encoder_class) # Encrypt serialised state. if self.cipher: state = self.cipher.encrypt(state) return topic, state
def test_all_domain_events(self): event_store = self.construct_event_store() # Check there are zero domain events in total. domain_events = event_store.all_domain_events() domain_events = list(domain_events) self.assertEqual(len(domain_events), 0) # Store a domain event. entity_id1 = uuid4() event1 = Example.Created(a=1, b=2, originator_id=entity_id1, originator_topic=get_topic(Example)) event_store.store(event1) # Store another domain event for the same entity. event1 = Example.AttributeChanged( a=1, b=2, originator_id=entity_id1, originator_version=1, __previous_hash__=event1.__event_hash__, ) event_store.store(event1) # Store a domain event for a different entity. entity_id2 = uuid4() event1 = Example.Created( originator_topic=get_topic(Example), originator_id=entity_id2, a=1, b=2, ) event_store.store(event1) # Check there are three domain events in total. domain_events = event_store.all_domain_events() domain_events = list(domain_events) self.assertEqual(len(domain_events), 3)
def test_get_domain_events(self): event_store = self.construct_event_store() # Check there are zero stored events in the repo. entity_id1 = uuid4() entity_events = event_store.get_domain_events(originator_id=entity_id1) entity_events = list(entity_events) self.assertEqual(0, len(entity_events)) # Check there are zero events in the event store, using iterator. entity_events = event_store.get_domain_events(originator_id=entity_id1, page_size=1) entity_events = list(entity_events) self.assertEqual(0, len(entity_events)) # Store a domain event. event1 = Example.Created(a=1, b=2, originator_id=entity_id1, originator_topic=get_topic(Example)) event_store.store(event1) # Check there is one event in the event store. entity_events = event_store.get_domain_events(originator_id=entity_id1) entity_events = list(entity_events) self.assertEqual(1, len(entity_events)) # Check there are two events in the event store, using iterator. entity_events = event_store.get_domain_events(originator_id=entity_id1, page_size=1) entity_events = list(entity_events) self.assertEqual(1, len(entity_events)) # Store another domain event. event1 = Example.AttributeChanged( a=1, b=2, originator_id=entity_id1, originator_version=1, ) event_store.store(event1) # Check there are two events in the event store. entity_events = event_store.get_domain_events(originator_id=entity_id1) entity_events = list(entity_events) self.assertEqual(2, len(entity_events)) # Check there are two events in the event store, using iterator. entity_events = event_store.get_domain_events(originator_id=entity_id1, page_size=1) entity_events = list(entity_events) self.assertEqual(2, len(entity_events))
def test_get_item(self) -> None: # Setup an event store. event_store = self.construct_event_store() # Put an event in the event store. entity_id = uuid4() event_store.store_events([ Example.Created( a=1, b=2, originator_id=entity_id, originator_topic=get_topic(Example), ) ]) # Construct a repository. event_sourced_repo: EventSourcedRepository[ Example, Example.Event] = EventSourcedRepository(event_store=event_store) # Check the entity attributes. example = event_sourced_repo[entity_id] self.assertEqual(1, example.a) self.assertEqual(2, example.b) self.assertEqual(entity_id, example.id) # Check class-specific request. example = event_sourced_repo.get_instance_of(Example, entity_id) self.assertEqual(1, example.a) self.assertEqual(2, example.b) self.assertEqual(entity_id, example.id) # Check class-specific request fails on type. class SubExample(Example): pass self.assertIsNone( event_sourced_repo.get_instance_of(SubExample, entity_id)) # Setup an example repository, using the subclass ExampleRepository. example_repo = ExampleRepository(event_store=event_store) # Check the repo has the example. self.assertIn(entity_id, example_repo) self.assertNotIn(uuid4(), example_repo) # Check the entity attributes. example = example_repo[entity_id] self.assertEqual(1, example.a) self.assertEqual(2, example.b) self.assertEqual(entity_id, example.id)
def test_subscribe_to_decorator(self): entity_id1 = uuid4() event1 = Example.Created( originator_id=entity_id1, originator_topic=get_topic(Example), a=1, b=2 ) event2 = Example.Discarded( originator_id=entity_id1, originator_version=1, ) handler = mock.Mock() # Check we can assert there are no event handlers subscribed. assert_event_handlers_empty() @subscribe_to(Example.Created) def test_handler(e): """Doc string""" handler(e) # Check the decorator doesn't mess with the function doc string. self.assertEqual('Doc string', test_handler.__doc__) # Check can fail to assert event handlers empty. self.assertRaises(EventHandlersNotEmptyError, assert_event_handlers_empty) # Check event is received when published individually. publish(event1) handler.assert_called_once_with(event1) # Check event of wrong type is not received. handler.reset_mock() publish(event2) self.assertFalse(handler.call_count) # Check a list of events can be filtered. handler.reset_mock() publish([event1, event2]) handler.assert_called_once_with(event1) handler.reset_mock() publish([event1, event1]) self.assertEqual(2, handler.call_count) handler.reset_mock() publish([event2, event2]) self.assertEqual(0, handler.call_count) _event_handlers.clear()