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
}