def open_account(self, full_name, email_address): account = BankAccount.open( full_name=full_name, email_address=email_address, ) self.save(account) return account.id
def test_legacy_save(self): # Open an account. account = BankAccount.open( full_name="Alice", email_address="*****@*****.**", ) events = account.collect_events() created_event = events[0] processing_event = ProcessingEvent(tracking=Tracking( application_name="upstream_app", notification_id=5, )) with warnings.catch_warnings(record=True) as w: policy_legacy_save(created_event, processing_event) # Verify deprecation warning. assert len(w) == 1 assert issubclass(w[-1].category, DeprecationWarning) assert "'save()' is deprecated, use 'collect_events()' instead" in str( w[-1].message) self.assertEqual(len(processing_event.events), 1) self.assertIsInstance( processing_event.events[0], EmailNotification.Created, )
def test_subclass_bank_account(self): # Open an account. account: BankAccount = BankAccount.open( full_name="Alice", email_address="*****@*****.**", ) # Check the created_on. assert account.created_on == account.modified_on # Check the initial balance. assert account.balance == 0 # Credit the account. account.append_transaction(Decimal("10.00")) # Check the modified_on time was updated. assert account.created_on < account.modified_on # Check the balance. assert account.balance == Decimal("10.00") # Credit the account again. account.append_transaction(Decimal("10.00")) # Check the balance. assert account.balance == Decimal("20.00") # Debit the account. account.append_transaction(Decimal("-15.00")) # Check the balance. assert account.balance == Decimal("5.00") # Fail to debit account (insufficient funds). with self.assertRaises(InsufficientFundsError): account.append_transaction(Decimal("-15.00")) # Increase the overdraft limit. account.set_overdraft_limit(Decimal("100.00")) # Debit the account. account.append_transaction(Decimal("-15.00")) # Check the balance. assert account.balance == Decimal("-10.00") # Close the account. account.close() # Fail to debit account (account closed). with self.assertRaises(AccountClosedError): account.append_transaction(Decimal("-15.00")) # Collect pending events. pending = account.collect_events() assert len(pending) == 7
def test(self): # Open an account. account = BankAccount.open( full_name="Alice", email_address="*****@*****.**", ) # Credit the account. account.append_transaction(Decimal("10.00")) account.append_transaction(Decimal("25.00")) account.append_transaction(Decimal("30.00")) # Collect pending events. pending = account.collect_events() # Construct event store. transcoder = JSONTranscoder() transcoder.register(UUIDAsHex()) transcoder.register(DecimalAsStr()) transcoder.register(DatetimeAsISO()) transcoder.register(EmailAddressAsStr()) recorder = SQLiteAggregateRecorder(SQLiteDatastore(":memory:")) event_store = EventStore( mapper=Mapper(transcoder), recorder=recorder, ) recorder.create_table() # Get last event. last_event = event_store.get(account.id, desc=True, limit=1) assert list(last_event) == [] # Store pending events. event_store.put(pending) # Get domain events. domain_events = event_store.get(account.id) # Reconstruct the bank account. copy = None for domain_event in domain_events: copy = domain_event.mutate(copy) # Check copy has correct attribute values. assert copy.id == account.id assert copy.balance == Decimal("65.00") # Get last event. events = event_store.get(account.id, desc=True, limit=1) events = list(events) assert len(events) == 1 last_event = events[0] assert last_event.originator_id == account.id assert type(last_event) == BankAccount.TransactionAppended
def test(self): # Open an account. account = BankAccount.open( full_name="Alice", email_address="*****@*****.**", ) # Credit the account. account.append_transaction(Decimal("10.00")) account.append_transaction(Decimal("25.00")) account.append_transaction(Decimal("30.00")) transcoder = JSONTranscoder() transcoder.register(UUIDAsHex()) transcoder.register(DecimalAsStr()) transcoder.register(DatetimeAsISO()) transcoder.register(EmailAddressAsStr()) snapshot_store = EventStore( mapper=Mapper(transcoder=transcoder), recorder=SQLiteAggregateRecorder( SQLiteDatastore(":memory:"), events_table_name="snapshots", ), ) snapshot_store.recorder.create_table() # Clear pending events. account.collect_events() # Take a snapshot. snapshot = Snapshot.take(account) self.assertNotIn("pending_events", snapshot.state) # Store snapshot. snapshot_store.put([snapshot]) # Get snapshot. snapshots = snapshot_store.get(account.id, desc=True, limit=1) snapshot = next(snapshots) assert isinstance(snapshot, Snapshot) # Reconstruct the bank account. copy = snapshot.mutate(None) assert isinstance(copy, BankAccount) # Check copy has correct attribute values. assert copy.id == account.id assert copy.balance == Decimal("65.00")
def test_policy(self): # Open an account. account = BankAccount.open( full_name="Alice", email_address="*****@*****.**", ) events = account.collect_events() created_event = events[0] processing_event = ProcessingEvent(tracking=Tracking( application_name="upstream_app", notification_id=5, )) policy(created_event, processing_event) self.assertEqual(len(processing_event.events), 1) self.assertIsInstance( processing_event.events[0], EmailNotification.Created, )
def test_process_event(self): class UUID5EmailNotification(Aggregate): def __init__(self, to, subject, message): self.to = to self.subject = subject self.message = message @staticmethod def create_id(to: str): return uuid5(NAMESPACE_URL, f"/emails/{to}") class UUID5EmailProcess(EmailProcess): def policy(self, domain_event, processing_event): if isinstance(domain_event, BankAccount.Opened): notification = UUID5EmailNotification( to=domain_event.email_address, subject="Your New Account", message="Dear {}, ...".format(domain_event.full_name), ) processing_event.collect_events(notification) bank_accounts = BankAccounts() email_process = UUID5EmailProcess() account = BankAccount.open( full_name="Alice", email_address="*****@*****.**", ) recordings = bank_accounts.save(account) self.assertEqual(len(recordings), 1) aggregate_event = recordings[0].domain_event notification = recordings[0].notification tracking = Tracking(bank_accounts.name, notification.id) # Process the event. email_process.process_event(aggregate_event, tracking) self.assertEqual( email_process.recorder.max_tracking_id(bank_accounts.name), notification.id) # Process the event again, ignore tracking integrity error. email_process.process_event(aggregate_event, tracking) self.assertEqual( email_process.recorder.max_tracking_id(bank_accounts.name), notification.id) # Create another event that will cause conflict with email processing. account = BankAccount.open( full_name="Alice", email_address="*****@*****.**", ) recordings = bank_accounts.save(account) # Process the event and expect an integrity error. aggregate_event = recordings[0].domain_event notification = recordings[0].notification tracking = Tracking(bank_accounts.name, notification.id) with self.assertRaises(IntegrityError): email_process.process_event(aggregate_event, tracking)
def test_with_snapshot_store(self) -> None: transcoder = JSONTranscoder() transcoder.register(UUIDAsHex()) transcoder.register(DecimalAsStr()) transcoder.register(DatetimeAsISO()) transcoder.register(EmailAddressAsStr()) event_recorder = SQLiteAggregateRecorder(SQLiteDatastore(":memory:")) event_recorder.create_table() event_store = EventStore( mapper=Mapper(transcoder=transcoder), recorder=event_recorder, ) snapshot_recorder = SQLiteAggregateRecorder( SQLiteDatastore(":memory:")) snapshot_recorder.create_table() snapshot_store = EventStore( mapper=Mapper(transcoder=transcoder), recorder=snapshot_recorder, ) repository = Repository(event_store, snapshot_store) # Check key error. with self.assertRaises(AggregateNotFound): repository.get(uuid4()) # Open an account. account = BankAccount.open( full_name="Alice", email_address="*****@*****.**", ) # Credit the account. account.append_transaction(Decimal("10.00")) account.append_transaction(Decimal("25.00")) account.append_transaction(Decimal("30.00")) # Collect pending events. pending = account.collect_events() # Store pending events. event_store.put(pending) copy = repository.get(account.id) assert isinstance(copy, BankAccount) # Check copy has correct attribute values. assert copy.id == account.id assert copy.balance == Decimal("65.00") snapshot = Snapshot( originator_id=account.id, originator_version=account.version, timestamp=Snapshot.create_timestamp(), topic=get_topic(type(account)), state=account.__dict__, ) snapshot_store.put([snapshot]) copy2 = repository.get(account.id) assert isinstance(copy2, BankAccount) # Check copy has correct attribute values. assert copy2.id == account.id assert copy2.balance == Decimal("65.00") # Credit the account. account.append_transaction(Decimal("10.00")) event_store.put(account.collect_events()) # Check copy has correct attribute values. copy3 = repository.get(account.id) assert isinstance(copy3, BankAccount) assert copy3.id == account.id assert copy3.balance == Decimal("75.00") # Check can get old version of account. copy4 = repository.get(account.id, version=copy.version) assert isinstance(copy4, BankAccount) assert copy4.balance == Decimal("65.00") copy5 = repository.get(account.id, version=1) assert isinstance(copy5, BankAccount) assert copy5.balance == Decimal("0.00") copy6 = repository.get(account.id, version=2) assert isinstance(copy6, BankAccount) assert copy6.balance == Decimal("10.00") copy7 = repository.get(account.id, version=3) assert isinstance(copy7, BankAccount) assert copy7.balance == Decimal("35.00"), copy7.balance copy8 = repository.get(account.id, version=4) assert isinstance(copy8, BankAccount) assert copy8.balance == Decimal("65.00"), copy8.balance
def test(self): # Construct transcoder. transcoder = JSONTranscoder() transcoder.register(UUIDAsHex()) transcoder.register(DecimalAsStr()) transcoder.register(DatetimeAsISO()) # Construct cipher. environment = Environment() environment[AESCipher.CIPHER_KEY] = AESCipher.create_key(16) cipher = AESCipher(environment) # Construct compressor. compressor = ZlibCompressor() # Construct mapper with cipher. mapper = Mapper(transcoder=transcoder, cipher=cipher) # Create a domain event. domain_event = BankAccount.TransactionAppended( originator_id=uuid4(), originator_version=123456, timestamp=BankAccount.TransactionAppended.create_timestamp(), amount=Decimal("10.00"), ) # Map from domain event. stored_event = mapper.from_domain_event(domain_event) # Map to domain event. copy = mapper.to_domain_event(stored_event) # Check values are not visible. assert "Alice" not in str(stored_event.state) # Check decrypted copy has correct values. assert copy.originator_id == domain_event.originator_id assert copy.originator_version == domain_event.originator_version assert copy.timestamp == domain_event.timestamp, copy.timestamp assert copy.originator_version == domain_event.originator_version assert len(stored_event.state) == 162, len(stored_event.state) # Construct mapper with cipher and compressor. mapper = Mapper( transcoder=transcoder, cipher=cipher, compressor=compressor, ) # Map from domain event. stored_event = mapper.from_domain_event(domain_event) # Map to domain event. copy = mapper.to_domain_event(stored_event) # Check decompressed copy has correct values. assert copy.originator_id == domain_event.originator_id assert copy.originator_version == domain_event.originator_version assert len(stored_event.state) in ( 135, 136, 137, 138, 139, 140, 141, 142, 143, ), len(stored_event.state)