using Books.Api.Domain; using Books.Api.Domain.BankConnections; using Books.Api.Domain.BankConnections.Events; using AwesomeAssertions; namespace Books.Api.Tests.Domain; /// /// Unit tests for BankConnectionAggregate domain logic. /// Tests aggregate behavior without EventFlow infrastructure. /// [Trait("Category", "Unit")] public class BankConnectionAggregateTests { #region Initiate Tests [Fact] public void Initiate_WithValidData_EmitsInitiatedEvent() { // Arrange var aggregate = new BankConnectionAggregate(BankConnectionId.New); // Act aggregate.Initiate("company-123", "Danske Bank", "auth-456", "https://callback.url", "state-789"); // Assert var uncommittedEvents = aggregate.UncommittedEvents.ToList(); uncommittedEvents.Should().ContainSingle(); var initiatedEvent = uncommittedEvents[0].AggregateEvent as BankConnectionInitiatedEvent; initiatedEvent.Should().NotBeNull(); initiatedEvent!.CompanyId.Should().Be("company-123"); initiatedEvent.AspspName.Should().Be("Danske Bank"); initiatedEvent.AuthorizationId.Should().Be("auth-456"); initiatedEvent.RedirectUrl.Should().Be("https://callback.url"); initiatedEvent.State.Should().Be("state-789"); } [Fact] public void Initiate_WithEmptyAspspName_ThrowsDomainException() { // Arrange var aggregate = new BankConnectionAggregate(BankConnectionId.New); // Act var act = () => aggregate.Initiate("company-123", " ", "auth-456", "https://callback.url", "state-789"); // Assert act.Should().Throw() .Where(e => e.Code == "ASPSP_NAME_REQUIRED"); } [Fact] public void Initiate_WhenAlreadyInitiated_ThrowsDomainException() { // Arrange var aggregate = CreateInitiatedConnection(); // Act var act = () => aggregate.Initiate("company-123", "Nordea", "auth-999", "https://other.url", "state-111"); // Assert act.Should().Throw() .Where(e => e.Code == "BANK_CONNECTION_ALREADY_INITIATED"); } #endregion #region Establish Tests [Fact] public void Establish_WhenInitiated_EmitsEstablishedEvent() { // Arrange var aggregate = CreateInitiatedConnection(); var accounts = new List { new("acc-1", "DK1234567890123456", "DKK", "Lønkonto"), new("acc-2", "DK9876543210987654", "DKK", "Opsparingskonto") }; var validUntil = DateTimeOffset.UtcNow.AddDays(90); // Act aggregate.Establish("session-123", validUntil, accounts); // Assert var uncommittedEvents = aggregate.UncommittedEvents.ToList(); uncommittedEvents.Should().HaveCount(2); // Initiated + Established var establishedEvent = uncommittedEvents[1].AggregateEvent as BankConnectionEstablishedEvent; establishedEvent.Should().NotBeNull(); establishedEvent!.SessionId.Should().Be("session-123"); establishedEvent.ValidUntil.Should().Be(validUntil); establishedEvent.Accounts.Should().HaveCount(2); } [Fact] public void Establish_WhenNotInitiated_ThrowsDomainException() { // Arrange var aggregate = new BankConnectionAggregate(BankConnectionId.New); var accounts = new List { new("acc-1", "DK1234", "DKK", null) }; // Act var act = () => aggregate.Establish("session-123", DateTimeOffset.UtcNow.AddDays(90), accounts); // Assert act.Should().Throw() .Where(e => e.Code == "BANK_CONNECTION_NOT_INITIATED"); } [Fact] public void Establish_WithEmptySessionId_ThrowsDomainException() { // Arrange var aggregate = CreateInitiatedConnection(); var accounts = new List { new("acc-1", "DK1234", "DKK", null) }; // Act var act = () => aggregate.Establish(" ", DateTimeOffset.UtcNow.AddDays(90), accounts); // Assert act.Should().Throw() .Where(e => e.Code == "SESSION_ID_REQUIRED"); } [Fact] public void Establish_WithNoAccounts_ThrowsDomainException() { // Arrange var aggregate = CreateInitiatedConnection(); var accounts = new List(); // Act var act = () => aggregate.Establish("session-123", DateTimeOffset.UtcNow.AddDays(90), accounts); // Assert act.Should().Throw() .Where(e => e.Code == "NO_ACCOUNTS_FOUND"); } [Fact] public void Establish_WhenAlreadyEstablished_ThrowsDomainException() { // Arrange var aggregate = CreateEstablishedConnection(); var accounts = new List { new("acc-1", "DK1234", "DKK", null) }; // Act var act = () => aggregate.Establish("session-456", DateTimeOffset.UtcNow.AddDays(90), accounts); // Assert act.Should().Throw() .Where(e => e.Code == "BANK_CONNECTION_ALREADY_ESTABLISHED"); } #endregion #region Fail Tests [Fact] public void Fail_WhenInitiated_EmitsFailedEvent() { // Arrange var aggregate = CreateInitiatedConnection(); // Act aggregate.Fail("User cancelled authorization"); // Assert var uncommittedEvents = aggregate.UncommittedEvents.ToList(); uncommittedEvents.Should().HaveCount(2); // Initiated + Failed var failedEvent = uncommittedEvents[1].AggregateEvent as BankConnectionFailedEvent; failedEvent.Should().NotBeNull(); failedEvent!.Reason.Should().Be("User cancelled authorization"); } [Fact] public void Fail_WhenNotInitiated_ThrowsDomainException() { // Arrange var aggregate = new BankConnectionAggregate(BankConnectionId.New); // Act var act = () => aggregate.Fail("Some reason"); // Assert act.Should().Throw() .Where(e => e.Code == "BANK_CONNECTION_NOT_INITIATED"); } #endregion #region Disconnect Tests [Fact] public void Disconnect_WhenEstablished_EmitsDisconnectedEvent() { // Arrange var aggregate = CreateEstablishedConnection(); // Act aggregate.Disconnect("User requested disconnection"); // Assert var uncommittedEvents = aggregate.UncommittedEvents.ToList(); uncommittedEvents.Should().HaveCount(3); // Initiated + Established + Disconnected var disconnectedEvent = uncommittedEvents[2].AggregateEvent as BankConnectionDisconnectedEvent; disconnectedEvent.Should().NotBeNull(); disconnectedEvent!.Reason.Should().Be("User requested disconnection"); } [Fact] public void Disconnect_WhenInitiated_EmitsDisconnectedEvent() { // Arrange var aggregate = CreateInitiatedConnection(); // Act aggregate.Disconnect("User cancelled flow"); // Assert var uncommittedEvents = aggregate.UncommittedEvents.ToList(); uncommittedEvents.Should().HaveCount(2); // Initiated + Disconnected var disconnectedEvent = uncommittedEvents[1].AggregateEvent as BankConnectionDisconnectedEvent; disconnectedEvent.Should().NotBeNull(); disconnectedEvent!.Reason.Should().Be("User cancelled flow"); } [Fact] public void Disconnect_WhenAlreadyDisconnected_Idempotent_DoesNothing() { // Arrange var aggregate = CreateDisconnectedConnection(); var initialEventCount = aggregate.UncommittedEvents.Count(); // Act aggregate.Disconnect("Trying again"); // Assert aggregate.UncommittedEvents.Count().Should().Be(initialEventCount); } #endregion #region LinkBankAccount Tests [Fact] public void LinkBankAccount_WhenEstablished_EmitsLinkedEvent() { // Arrange var aggregate = CreateEstablishedConnection(); var importDate = new DateOnly(2023, 1, 1); // Act aggregate.LinkBankAccount("acc-1", "ledger-1000", importDate); // Assert var uncommittedEvents = aggregate.UncommittedEvents.ToList(); var linkedEvent = uncommittedEvents.Last().AggregateEvent as BankAccountLinkedEvent; linkedEvent.Should().NotBeNull(); linkedEvent!.BankAccountId.Should().Be("acc-1"); linkedEvent.LinkedAccountId.Should().Be("ledger-1000"); linkedEvent.ImportFromDate.Should().Be(importDate); } [Fact] public void LinkBankAccount_WhenNotEstablished_ThrowsDomainException() { // Arrange var aggregate = CreateInitiatedConnection(); // Act var act = () => aggregate.LinkBankAccount("acc-1", "ledger-1000"); // Assert act.Should().Throw() .Where(e => e.Code == "BANK_CONNECTION_NOT_ACTIVE"); } [Fact] public void LinkBankAccount_WithUnknownAccountId_ThrowsDomainException() { // Arrange var aggregate = CreateEstablishedConnection(); // Act var act = () => aggregate.LinkBankAccount("unknown-acc", "ledger-1000"); // Assert act.Should().Throw() .Where(e => e.Code == "BANK_ACCOUNT_NOT_FOUND"); } #endregion #region ReInitiate Tests [Fact] public void ReInitiate_WhenDisconnected_EmitsReInitiatedEvent() { // Arrange var aggregate = CreateDisconnectedConnection(); // Act aggregate.ReInitiate("new-auth", "url", "new-state"); // Assert var uncommittedEvents = aggregate.UncommittedEvents.ToList(); var event_ = uncommittedEvents.Last().AggregateEvent as BankConnectionReInitiatedEvent; event_.Should().NotBeNull(); event_!.AuthorizationId.Should().Be("new-auth"); event_.State.Should().Be("new-state"); } [Fact] public void ReInitiate_WhenActive_ThrowsDomainException() { // Arrange var aggregate = CreateEstablishedConnection(); // Act var act = () => aggregate.ReInitiate("auth", "url", "state"); // Assert act.Should().Throw() .Where(e => e.Code == "BANK_CONNECTION_STILL_ACTIVE"); } #endregion #region Archive Tests [Fact] public void Archive_WhenDisconnected_EmitsArchivedEvent() { // Arrange var aggregate = CreateDisconnectedConnection(); // Act aggregate.Archive(); // Assert var uncommittedEvents = aggregate.UncommittedEvents.ToList(); var event_ = uncommittedEvents.Last().AggregateEvent as BankConnectionArchivedEvent; event_.Should().NotBeNull(); } [Fact] public void Archive_WhenActive_ThrowsDomainException() { // Arrange var aggregate = CreateEstablishedConnection(); // Act var act = () => aggregate.Archive(); // Assert act.Should().Throw() .Where(e => e.Code == "BANK_CONNECTION_STILL_ACTIVE"); } #endregion #region Refresh Tests [Fact] public void Refresh_WhenEstablished_EmitsRefreshedEvent() { // Arrange var aggregate = CreateEstablishedConnection(); var newValidUntil = DateTimeOffset.UtcNow.AddDays(180); // Act aggregate.Refresh("new-session-456", newValidUntil); // Assert var uncommittedEvents = aggregate.UncommittedEvents.ToList(); uncommittedEvents.Should().HaveCount(3); // Initiated + Established + Refreshed var refreshedEvent = uncommittedEvents[2].AggregateEvent as BankConnectionRefreshedEvent; refreshedEvent.Should().NotBeNull(); refreshedEvent!.NewSessionId.Should().Be("new-session-456"); refreshedEvent.ValidUntil.Should().Be(newValidUntil); } [Fact] public void Refresh_WhenNotEstablished_ThrowsDomainException() { // Arrange var aggregate = CreateInitiatedConnection(); // Act var act = () => aggregate.Refresh("new-session-456", DateTimeOffset.UtcNow.AddDays(90)); // Assert act.Should().Throw() .Where(e => e.Code == "BANK_CONNECTION_NOT_ACTIVE"); } [Fact] public void Refresh_WhenDisconnected_ThrowsDomainException() { // Arrange var aggregate = CreateDisconnectedConnection(); // Act var act = () => aggregate.Refresh("new-session-456", DateTimeOffset.UtcNow.AddDays(90)); // Assert act.Should().Throw() .Where(e => e.Code == "BANK_CONNECTION_NOT_ACTIVE"); } #endregion #region IsActive Property Tests [Fact] public void IsActive_WhenEstablishedAndNotExpired_ReturnsTrue() { // Arrange var aggregate = CreateEstablishedConnection(); // Assert aggregate.IsActive.Should().BeTrue(); } [Fact] public void IsActive_WhenNotEstablished_ReturnsFalse() { // Arrange var aggregate = CreateInitiatedConnection(); // Assert aggregate.IsActive.Should().BeFalse(); } [Fact] public void IsActive_WhenDisconnected_ReturnsFalse() { // Arrange var aggregate = CreateDisconnectedConnection(); // Assert aggregate.IsActive.Should().BeFalse(); } #endregion #region Helper Methods private static BankConnectionAggregate CreateInitiatedConnection() { var aggregate = new BankConnectionAggregate(BankConnectionId.New); aggregate.Initiate("company-123", "Danske Bank", "auth-456", "https://callback.url", "state-789"); return aggregate; } private static BankConnectionAggregate CreateEstablishedConnection() { var aggregate = CreateInitiatedConnection(); var accounts = new List { new("acc-1", "DK1234567890123456", "DKK", "Lønkonto") }; aggregate.Establish("session-123", DateTimeOffset.UtcNow.AddDays(90), accounts); return aggregate; } private static BankConnectionAggregate CreateDisconnectedConnection() { var aggregate = CreateEstablishedConnection(); aggregate.Disconnect("User requested disconnection"); return aggregate; } #endregion }