This commit includes all previously untracked backend files: Domain: - Accounts, Attachments, BankConnections, Customers - FiscalYears, Invoices, JournalEntryDrafts - Orders, Products, UserAccess Commands & Handlers: - Full CQRS command structure for all domains Repositories: - PostgreSQL repositories for all read models - Bank transaction and ledger repositories GraphQL: - Input types, scalars, and types for all entities - Mutations and queries Infrastructure: - Banking integration (Enable Banking client) - File storage, Invoicing, Reporting, SAF-T export - Database migrations (003-029) Tests: - Integration tests for GraphQL endpoints - Domain tests - Invoicing and reporting tests Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
485 lines
14 KiB
C#
485 lines
14 KiB
C#
using Books.Api.Domain;
|
|
using Books.Api.Domain.BankConnections;
|
|
using Books.Api.Domain.BankConnections.Events;
|
|
using AwesomeAssertions;
|
|
|
|
namespace Books.Api.Tests.Domain;
|
|
|
|
/// <summary>
|
|
/// Unit tests for BankConnectionAggregate domain logic.
|
|
/// Tests aggregate behavior without EventFlow infrastructure.
|
|
/// </summary>
|
|
[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<DomainException>()
|
|
.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<DomainException>()
|
|
.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<BankAccountInfo>
|
|
{
|
|
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<BankAccountInfo> { new("acc-1", "DK1234", "DKK", null) };
|
|
|
|
// Act
|
|
var act = () => aggregate.Establish("session-123", DateTimeOffset.UtcNow.AddDays(90), accounts);
|
|
|
|
// Assert
|
|
act.Should().Throw<DomainException>()
|
|
.Where(e => e.Code == "BANK_CONNECTION_NOT_INITIATED");
|
|
}
|
|
|
|
[Fact]
|
|
public void Establish_WithEmptySessionId_ThrowsDomainException()
|
|
{
|
|
// Arrange
|
|
var aggregate = CreateInitiatedConnection();
|
|
var accounts = new List<BankAccountInfo> { new("acc-1", "DK1234", "DKK", null) };
|
|
|
|
// Act
|
|
var act = () => aggregate.Establish(" ", DateTimeOffset.UtcNow.AddDays(90), accounts);
|
|
|
|
// Assert
|
|
act.Should().Throw<DomainException>()
|
|
.Where(e => e.Code == "SESSION_ID_REQUIRED");
|
|
}
|
|
|
|
[Fact]
|
|
public void Establish_WithNoAccounts_ThrowsDomainException()
|
|
{
|
|
// Arrange
|
|
var aggregate = CreateInitiatedConnection();
|
|
var accounts = new List<BankAccountInfo>();
|
|
|
|
// Act
|
|
var act = () => aggregate.Establish("session-123", DateTimeOffset.UtcNow.AddDays(90), accounts);
|
|
|
|
// Assert
|
|
act.Should().Throw<DomainException>()
|
|
.Where(e => e.Code == "NO_ACCOUNTS_FOUND");
|
|
}
|
|
|
|
[Fact]
|
|
public void Establish_WhenAlreadyEstablished_ThrowsDomainException()
|
|
{
|
|
// Arrange
|
|
var aggregate = CreateEstablishedConnection();
|
|
var accounts = new List<BankAccountInfo> { new("acc-1", "DK1234", "DKK", null) };
|
|
|
|
// Act
|
|
var act = () => aggregate.Establish("session-456", DateTimeOffset.UtcNow.AddDays(90), accounts);
|
|
|
|
// Assert
|
|
act.Should().Throw<DomainException>()
|
|
.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<DomainException>()
|
|
.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<DomainException>()
|
|
.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<DomainException>()
|
|
.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<DomainException>()
|
|
.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<DomainException>()
|
|
.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<DomainException>()
|
|
.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<DomainException>()
|
|
.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<BankAccountInfo>
|
|
{
|
|
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
|
|
}
|