books/backend/Books.Api.Tests/Domain/BankConnectionAggregateTests.cs
Nicolaj Hartmann 1f75c5d791 Add all backend domain, commands, repositories, and tests
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>
2026-01-30 22:19:42 +01:00

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
}