Added more unit tests.

This commit is contained in:
2026-07-30 13:36:05 +02:00
parent 882dd7afe1
commit b37ca9222d
3 changed files with 133 additions and 16 deletions
@@ -2,6 +2,7 @@ using FinancialApi.Application.Commands;
using FinancialApi.Application.Handlers;
using FinancialApi.Application.Models;
using FinancialApi.Domain.Entities;
using FinancialApi.Domain.Exceptions;
using FluentAssertions;
using FluentAssertions.Execution;
using Microsoft.Extensions.Time.Testing;
@@ -10,29 +11,144 @@ namespace FinancialApi.Application.UnitTests;
public class TransferFundsHandlerTests
{
public class ApplyFeeHandlerTests
public class LiabilityAccount
{
[Fact]
public void GivenValidAccounts_BasicFee_ShouldValidateEverything()
[Theory]
[InlineData(100.0, 20.0, 80.0)]
[InlineData(199.78, 10.0, 189.78)]
[InlineData(200000, 0.09, 199999.91)]
[InlineData(1000, 1000, 0)]
public void GivenStartingBalanceGreaterThanTransfer_ApplyFee_ShouldBeExpectedBalance(
decimal startingBalance,
decimal feeAmount,
decimal expectedBalance
)
{
// Arrange
var fakeTimeProvider = new FakeTimeProvider();
var command = new TransferFundsCommand(1, 2, 100);
var sourceAccount = new Account(1, 100, AccountType.Liability);
var destAccount = new Account(1, 100, AccountType.Liability);
var transferContext = new TransferContext(sourceAccount, destAccount, fakeTimeProvider.GetUtcNow());
var timeProvider = new FakeTimeProvider();
var command = new TransferFundsCommand(1, 2, feeAmount);
var sourceAccount = new Account(1, startingBalance, AccountType.Liability);
var destAccount = new Account(2, 0, AccountType.Liability);
var transferContext = new TransferContext(sourceAccount, destAccount, timeProvider.GetUtcNow());
// Act
var (sourceWrite, destWrite, journalWrite, @event) = TransferFundsHandler.Handle(command, transferContext);
var (customerWrite, revenueWrite, journalWrite, @event) =
TransferFundsHandler.Handle(command, transferContext);
// Assert
using var _ = new AssertionScope();
sourceWrite.Entity.Balance.Should()
.Be(0);
destWrite.Entity.Balance.Should()
.Be(200);
journalWrite.Entity.Lines.Should()
.HaveCountGreaterThanOrEqualTo(2);
customerWrite.Entity.Balance.Should()
.Be(expectedBalance);
}
[Theory]
[InlineData(100.0, 110.0)]
[InlineData(199.78, 200.0)]
[InlineData(200000, 200000.01)]
[InlineData(1000, 1999999)]
public void GivenStartingBalanceLessThanTransfer_ApplyFee_ShouldFail(decimal startingBalance, decimal feeAmount)
{
// Arrange
var timeProvider = new FakeTimeProvider();
var command = new TransferFundsCommand(1, 2, feeAmount);
var sourceAccount = new Account(1, startingBalance, AccountType.Liability);
var destAccount = new Account(2, 0, AccountType.Liability);
var transferContext = new TransferContext(sourceAccount, destAccount, timeProvider.GetUtcNow());
// Act
var act = () => TransferFundsHandler.Handle(command, transferContext);
// Assert
using var _ = new AssertionScope();
act.Should()
.Throw<InsufficientFundsException>();
}
}
public class CreditAccount
{
[Theory]
[InlineData(100.0, 20.0, 80.0)]
[InlineData(199.78, 10.0, 189.78)]
[InlineData(200000, 0.09, 199999.91)]
[InlineData(1000, 1000, 0)]
public void GivenStartingBalanceGreaterThanFee_ApplyFee_ShouldBeExpectedBalance(
decimal startingBalance,
decimal feeAmount,
decimal expectedBalance
)
{
// Arrange
var timeProvider = new FakeTimeProvider();
var command = new TransferFundsCommand(1, 2, feeAmount);
var sourceAccount = new Account(1, startingBalance, AccountType.Credit);
var destAccount = new Account(2, 0, AccountType.Credit);
var transferContext = new TransferContext(sourceAccount, destAccount, timeProvider.GetUtcNow());
// Act
var (customerWrite, revenueWrite, journalWrite, @event) =
TransferFundsHandler.Handle(command, transferContext);
// Assert
using var _ = new AssertionScope();
customerWrite.Entity.Balance.Should()
.Be(expectedBalance);
}
[Theory]
[InlineData(100.0, 110.0, -10.0)]
[InlineData(199.78, 200.0, -0.22)]
[InlineData(200000, 200000.01, -0.01)]
[InlineData(1000, 1999999, -1998999)]
public void GivenStartingBalanceLessThanFee_ApplyFee_ShouldSucceed(
decimal startingBalance,
decimal feeAmount,
decimal expectedBalance
)
{
// Arrange
var timeProvider = new FakeTimeProvider();
var command = new TransferFundsCommand(1, 2, feeAmount);
var sourceAccount = new Account(1, startingBalance, AccountType.Credit);
var destAccount = new Account(2, 0, AccountType.Credit);
var transferContext = new TransferContext(sourceAccount, destAccount, timeProvider.GetUtcNow());
// Act
var act = () => TransferFundsHandler.Handle(command, transferContext);
// Assert
using var _ = new AssertionScope();
act.Should()
.NotThrow();
}
[Theory]
[InlineData(100.0, 110.0, -10.0)]
[InlineData(199.78, 200.0, -0.22)]
[InlineData(200000, 200000.01, -0.01)]
[InlineData(1000, 1999999, -1998999)]
public void GivenStartingBalanceLessThanFee_ApplyFee_ShouldBeExpectedBalance(
decimal startingBalance,
decimal feeAmount,
decimal expectedBalance
)
{
// Arrange
var timeProvider = new FakeTimeProvider();
var command = new TransferFundsCommand(1, 2, feeAmount);
var sourceAccount = new Account(1, startingBalance, AccountType.Credit);
var destAccount = new Account(2, 0, AccountType.Credit);
var transferContext = new TransferContext(sourceAccount, destAccount, timeProvider.GetUtcNow());
// Act
var (customerWrite, revenueWrite, journalWrite, @event) =
TransferFundsHandler.Handle(command, transferContext);
// Assert
using var _ = new AssertionScope();
customerWrite.Entity.Balance.Should()
.Be(expectedBalance);
}
}
}
@@ -16,7 +16,7 @@ public static class ApplyFeeHandler
var dest = await query.FindByIdAsync(99999);
if (source == null || dest == null)
{
throw new InvalidOperationException($"Cannot process transfer. Account(s) not found.");
throw new InvalidOperationException("Cannot process transfer. Account(s) not found.");
}
return new FeeContext(source, dest, timeProvider.GetUtcNow());
@@ -1,6 +1,7 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AConfiguredTaskAwaitable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2026_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fe929cb9673c947ad9269facdac90006adb3400_003F14_003Fd939fe0a_003FConfiguredTaskAwaitable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AConstructorBindingFactory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fda476e261fcf49b394c1074b25b4cdce2b2960_003Fd4_003Fb1aa95c1_003FConstructorBindingFactory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AConstructorBindingFactory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2026_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fda476e261fcf49b394c1074b25b4cdce2b2960_003Fb3_003F33475782_003FConstructorBindingFactory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADbContext_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2026_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fda476e261fcf49b394c1074b25b4cdce2b2960_003F37_003F1b1010ed_003FDbContext_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ALateBoundTestFramework_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fcffa676d9bfc6c7a1c9d0d25ead15b3b0c9e12e3187bf397eaa52260e5fd91_003FLateBoundTestFramework_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMessageContext_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2026_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fb263759e5fefb34bcd589e63aeba791cef6f591ecbc93d1a7e62befa9e8bc6_003FMessageContext_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>