Test: add comprehensive unit test project for services
Add MoneyMap.Tests project with xUnit tests for all new services: - AccountServiceTests: Account retrieval, stats, validation, deletion - CardServiceTests: Card retrieval, stats, validation, deletion - MerchantServiceTests: Merchant CRUD operations - ReferenceDataServiceTests: Reference data retrieval - TransactionServiceTests: Duplicate detection, retrieval, deletion - TransactionStatisticsServiceTests: Statistics calculations - ReceiptMatchingServiceTests: Receipt-to-transaction matching logic - DbContextHelper: In-memory database context factory for test isolation Uses xUnit, Moq, and EF Core InMemory database. Solution file updated to include test project. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
27
MoneyMap.Tests/MoneyMap.Tests.csproj
Normal file
27
MoneyMap.Tests/MoneyMap.Tests.csproj
Normal file
@@ -0,0 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MoneyMap\MoneyMap.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
237
MoneyMap.Tests/Services/AccountServiceTests.cs
Normal file
237
MoneyMap.Tests/Services/AccountServiceTests.cs
Normal file
@@ -0,0 +1,237 @@
|
||||
using MoneyMap.Models;
|
||||
using MoneyMap.Services;
|
||||
using MoneyMap.Tests.TestHelpers;
|
||||
using Xunit;
|
||||
|
||||
namespace MoneyMap.Tests.Services;
|
||||
|
||||
public class AccountServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task GetAllAccountsWithStatsAsync_ReturnsAccountsWithTransactionCounts()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new AccountService(context);
|
||||
|
||||
var account1 = new Account
|
||||
{
|
||||
Id = 1,
|
||||
Institution = "Bank A",
|
||||
AccountType = AccountType.Checking,
|
||||
Last4 = "1234",
|
||||
Owner = "John Doe"
|
||||
};
|
||||
var account2 = new Account
|
||||
{
|
||||
Id = 2,
|
||||
Institution = "Bank B",
|
||||
AccountType = AccountType.Savings,
|
||||
Last4 = "5678",
|
||||
Owner = "Jane Smith"
|
||||
};
|
||||
context.Accounts.AddRange(account1, account2);
|
||||
|
||||
var transaction1 = new Transaction
|
||||
{
|
||||
Date = DateTime.Now,
|
||||
Amount = -50.00m,
|
||||
Name = "Test",
|
||||
Memo = "Test",
|
||||
AccountId = 1
|
||||
};
|
||||
var transaction2 = new Transaction
|
||||
{
|
||||
Date = DateTime.Now,
|
||||
Amount = -25.00m,
|
||||
Name = "Test",
|
||||
Memo = "Test",
|
||||
AccountId = 1
|
||||
};
|
||||
context.Transactions.AddRange(transaction1, transaction2);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await service.GetAllAccountsWithStatsAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, result.Count);
|
||||
var account1Stats = result.First(a => a.Id == 1);
|
||||
Assert.Equal(2, account1Stats.TransactionCount);
|
||||
var account2Stats = result.First(a => a.Id == 2);
|
||||
Assert.Equal(0, account2Stats.TransactionCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanDeleteAccountAsync_ReturnsFalse_WhenAccountHasTransactions()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new AccountService(context);
|
||||
|
||||
var account = new Account
|
||||
{
|
||||
Id = 1,
|
||||
Institution = "Test Bank",
|
||||
AccountType = AccountType.Checking,
|
||||
Last4 = "1234",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Accounts.Add(account);
|
||||
|
||||
var transaction = new Transaction
|
||||
{
|
||||
Date = DateTime.Now,
|
||||
Amount = -50.00m,
|
||||
Name = "Test",
|
||||
Memo = "Test",
|
||||
AccountId = 1
|
||||
};
|
||||
context.Transactions.Add(transaction);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await service.CanDeleteAccountAsync(1);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.CanDelete);
|
||||
Assert.Contains("transaction", result.Reason, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanDeleteAccountAsync_ReturnsTrue_WhenAccountHasNoTransactions()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new AccountService(context);
|
||||
|
||||
var account = new Account
|
||||
{
|
||||
Id = 1,
|
||||
Institution = "Test Bank",
|
||||
AccountType = AccountType.Checking,
|
||||
Last4 = "1234",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Accounts.Add(account);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await service.CanDeleteAccountAsync(1);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.CanDelete);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteAccountAsync_SuccessfullyDeletesAccount_WhenNoTransactions()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new AccountService(context);
|
||||
|
||||
var account = new Account
|
||||
{
|
||||
Id = 1,
|
||||
Institution = "Test Bank",
|
||||
AccountType = AccountType.Checking,
|
||||
Last4 = "1234",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Accounts.Add(account);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await service.DeleteAccountAsync(1);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
Assert.Empty(context.Accounts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteAccountAsync_FailsToDelete_WhenAccountHasTransactions()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new AccountService(context);
|
||||
|
||||
var account = new Account
|
||||
{
|
||||
Id = 1,
|
||||
Institution = "Test Bank",
|
||||
AccountType = AccountType.Checking,
|
||||
Last4 = "1234",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Accounts.Add(account);
|
||||
|
||||
var transaction = new Transaction
|
||||
{
|
||||
Date = DateTime.Now,
|
||||
Amount = -50.00m,
|
||||
Name = "Test",
|
||||
Memo = "Test",
|
||||
AccountId = 1
|
||||
};
|
||||
context.Transactions.Add(transaction);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await service.DeleteAccountAsync(1);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
Assert.Single(context.Accounts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAccountDetailsAsync_ReturnsFullDetails_WithCardsAndCounts()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new AccountService(context);
|
||||
|
||||
var account = new Account
|
||||
{
|
||||
Id = 1,
|
||||
Institution = "Test Bank",
|
||||
AccountType = AccountType.Checking,
|
||||
Last4 = "1234",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Accounts.Add(account);
|
||||
|
||||
var card = new Card
|
||||
{
|
||||
Id = 1,
|
||||
AccountId = 1,
|
||||
Issuer = "VISA",
|
||||
Last4 = "9999",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Cards.Add(card);
|
||||
|
||||
var transaction = new Transaction
|
||||
{
|
||||
Date = DateTime.Now,
|
||||
Amount = -50.00m,
|
||||
Name = "Test",
|
||||
Memo = "Test",
|
||||
AccountId = 1,
|
||||
CardId = 1
|
||||
};
|
||||
context.Transactions.Add(transaction);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await service.GetAccountDetailsAsync(1);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(1, result.Account.Id);
|
||||
Assert.Single(result.Cards);
|
||||
Assert.Equal(1, result.Cards[0].TransactionCount);
|
||||
Assert.Equal(1, result.TransactionCount);
|
||||
}
|
||||
}
|
||||
232
MoneyMap.Tests/Services/CardServiceTests.cs
Normal file
232
MoneyMap.Tests/Services/CardServiceTests.cs
Normal file
@@ -0,0 +1,232 @@
|
||||
using MoneyMap.Models;
|
||||
using MoneyMap.Services;
|
||||
using MoneyMap.Tests.TestHelpers;
|
||||
using Xunit;
|
||||
|
||||
namespace MoneyMap.Tests.Services;
|
||||
|
||||
public class CardServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task GetAllCardsWithStatsAsync_ReturnsCardsWithTransactionCounts()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new CardService(context);
|
||||
|
||||
var account = new Account
|
||||
{
|
||||
Id = 1,
|
||||
Institution = "Test Bank",
|
||||
AccountType = AccountType.Checking,
|
||||
Last4 = "1234",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Accounts.Add(account);
|
||||
|
||||
var card1 = new Card
|
||||
{
|
||||
Id = 1,
|
||||
AccountId = 1,
|
||||
Issuer = "VISA",
|
||||
Last4 = "1111",
|
||||
Owner = "John Doe"
|
||||
};
|
||||
var card2 = new Card
|
||||
{
|
||||
Id = 2,
|
||||
AccountId = 1,
|
||||
Issuer = "Mastercard",
|
||||
Last4 = "2222",
|
||||
Owner = "Jane Smith"
|
||||
};
|
||||
context.Cards.AddRange(card1, card2);
|
||||
|
||||
var transaction = new Transaction
|
||||
{
|
||||
Date = DateTime.Now,
|
||||
Amount = -50.00m,
|
||||
Name = "Test",
|
||||
Memo = "Test",
|
||||
AccountId = 1,
|
||||
CardId = 1
|
||||
};
|
||||
context.Transactions.Add(transaction);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await service.GetAllCardsWithStatsAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, result.Count);
|
||||
var card1Stats = result.First(c => c.Card.Id == 1);
|
||||
Assert.Equal(1, card1Stats.TransactionCount);
|
||||
var card2Stats = result.First(c => c.Card.Id == 2);
|
||||
Assert.Equal(0, card2Stats.TransactionCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanDeleteCardAsync_ReturnsFalse_WhenCardHasTransactions()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new CardService(context);
|
||||
|
||||
var account = new Account
|
||||
{
|
||||
Id = 1,
|
||||
Institution = "Test Bank",
|
||||
AccountType = AccountType.Checking,
|
||||
Last4 = "1234",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Accounts.Add(account);
|
||||
|
||||
var card = new Card
|
||||
{
|
||||
Id = 1,
|
||||
AccountId = 1,
|
||||
Issuer = "VISA",
|
||||
Last4 = "9999",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Cards.Add(card);
|
||||
|
||||
var transaction = new Transaction
|
||||
{
|
||||
Date = DateTime.Now,
|
||||
Amount = -50.00m,
|
||||
Name = "Test",
|
||||
Memo = "Test",
|
||||
AccountId = 1,
|
||||
CardId = 1
|
||||
};
|
||||
context.Transactions.Add(transaction);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await service.CanDeleteCardAsync(1);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.CanDelete);
|
||||
Assert.Contains("transaction", result.Reason, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanDeleteCardAsync_ReturnsTrue_WhenCardHasNoTransactions()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new CardService(context);
|
||||
|
||||
var account = new Account
|
||||
{
|
||||
Id = 1,
|
||||
Institution = "Test Bank",
|
||||
AccountType = AccountType.Checking,
|
||||
Last4 = "1234",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Accounts.Add(account);
|
||||
|
||||
var card = new Card
|
||||
{
|
||||
Id = 1,
|
||||
AccountId = 1,
|
||||
Issuer = "VISA",
|
||||
Last4 = "9999",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Cards.Add(card);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await service.CanDeleteCardAsync(1);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.CanDelete);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteCardAsync_SuccessfullyDeletesCard_WhenNoTransactions()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new CardService(context);
|
||||
|
||||
var account = new Account
|
||||
{
|
||||
Id = 1,
|
||||
Institution = "Test Bank",
|
||||
AccountType = AccountType.Checking,
|
||||
Last4 = "1234",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Accounts.Add(account);
|
||||
|
||||
var card = new Card
|
||||
{
|
||||
Id = 1,
|
||||
AccountId = 1,
|
||||
Issuer = "VISA",
|
||||
Last4 = "9999",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Cards.Add(card);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await service.DeleteCardAsync(1);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
Assert.Empty(context.Cards);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteCardAsync_FailsToDelete_WhenCardHasTransactions()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new CardService(context);
|
||||
|
||||
var account = new Account
|
||||
{
|
||||
Id = 1,
|
||||
Institution = "Test Bank",
|
||||
AccountType = AccountType.Checking,
|
||||
Last4 = "1234",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Accounts.Add(account);
|
||||
|
||||
var card = new Card
|
||||
{
|
||||
Id = 1,
|
||||
AccountId = 1,
|
||||
Issuer = "VISA",
|
||||
Last4 = "9999",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Cards.Add(card);
|
||||
|
||||
var transaction = new Transaction
|
||||
{
|
||||
Date = DateTime.Now,
|
||||
Amount = -50.00m,
|
||||
Name = "Test",
|
||||
Memo = "Test",
|
||||
AccountId = 1,
|
||||
CardId = 1
|
||||
};
|
||||
context.Transactions.Add(transaction);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await service.DeleteCardAsync(1);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
Assert.Single(context.Cards);
|
||||
}
|
||||
}
|
||||
189
MoneyMap.Tests/Services/MerchantServiceTests.cs
Normal file
189
MoneyMap.Tests/Services/MerchantServiceTests.cs
Normal file
@@ -0,0 +1,189 @@
|
||||
using MoneyMap.Models;
|
||||
using MoneyMap.Services;
|
||||
using MoneyMap.Tests.TestHelpers;
|
||||
using Xunit;
|
||||
|
||||
namespace MoneyMap.Tests.Services;
|
||||
|
||||
public class MerchantServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task GetOrCreateAsync_CreatesNewMerchant_WhenDoesNotExist()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new MerchantService(context);
|
||||
|
||||
// Act
|
||||
var result = await service.GetOrCreateAsync("Walmart");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("Walmart", result.Name);
|
||||
Assert.Single(context.Merchants);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetOrCreateAsync_ReturnsExistingMerchant_WhenExists()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new MerchantService(context);
|
||||
|
||||
var existingMerchant = new Merchant { Id = 1, Name = "Walmart" };
|
||||
context.Merchants.Add(existingMerchant);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await service.GetOrCreateAsync("Walmart");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(1, result.Id);
|
||||
Assert.Single(context.Merchants);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAllMerchantsWithStatsAsync_ReturnsMerchantsWithStats()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new MerchantService(context);
|
||||
|
||||
var account = new Account
|
||||
{
|
||||
Id = 1,
|
||||
Institution = "Test Bank",
|
||||
AccountType = AccountType.Checking,
|
||||
Last4 = "1234",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Accounts.Add(account);
|
||||
|
||||
var merchant1 = new Merchant { Id = 1, Name = "Walmart" };
|
||||
var merchant2 = new Merchant { Id = 2, Name = "Target" };
|
||||
context.Merchants.AddRange(merchant1, merchant2);
|
||||
|
||||
var transaction = new Transaction
|
||||
{
|
||||
Date = DateTime.Now,
|
||||
Amount = -50.00m,
|
||||
Name = "Test",
|
||||
Memo = "Test",
|
||||
AccountId = 1,
|
||||
MerchantId = 1
|
||||
};
|
||||
context.Transactions.Add(transaction);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await service.GetAllMerchantsWithStatsAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, result.Count);
|
||||
var walmart = result.First(m => m.Name == "Walmart");
|
||||
Assert.Equal(1, walmart.TransactionCount);
|
||||
var target = result.First(m => m.Name == "Target");
|
||||
Assert.Equal(0, target.TransactionCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateMerchantAsync_SuccessfullyUpdatesName()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new MerchantService(context);
|
||||
|
||||
var merchant = new Merchant { Id = 1, Name = "Walmart" };
|
||||
context.Merchants.Add(merchant);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await service.UpdateMerchantAsync(1, "Walmart Supercenter");
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
var updated = await context.Merchants.FindAsync(1);
|
||||
Assert.Equal("Walmart Supercenter", updated!.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateMerchantAsync_FailsWhenDuplicateName()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new MerchantService(context);
|
||||
|
||||
var merchant1 = new Merchant { Id = 1, Name = "Walmart" };
|
||||
var merchant2 = new Merchant { Id = 2, Name = "Target" };
|
||||
context.Merchants.AddRange(merchant1, merchant2);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await service.UpdateMerchantAsync(1, "Target");
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
Assert.Contains("already exists", result.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteMerchantAsync_SuccessfullyDeletes()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new MerchantService(context);
|
||||
|
||||
var merchant = new Merchant { Id = 1, Name = "Walmart" };
|
||||
context.Merchants.Add(merchant);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await service.DeleteMerchantAsync(1);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
Assert.Empty(context.Merchants);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteMerchantAsync_ReportsTransactionAndMappingCounts()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new MerchantService(context);
|
||||
|
||||
var account = new Account
|
||||
{
|
||||
Id = 1,
|
||||
Institution = "Test Bank",
|
||||
AccountType = AccountType.Checking,
|
||||
Last4 = "1234",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Accounts.Add(account);
|
||||
|
||||
var merchant = new Merchant { Id = 1, Name = "Walmart" };
|
||||
context.Merchants.Add(merchant);
|
||||
|
||||
var transaction = new Transaction
|
||||
{
|
||||
Date = DateTime.Now,
|
||||
Amount = -50.00m,
|
||||
Name = "Test",
|
||||
Memo = "Test",
|
||||
AccountId = 1,
|
||||
MerchantId = 1
|
||||
};
|
||||
context.Transactions.Add(transaction);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await service.DeleteMerchantAsync(1);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(1, result.TransactionCount);
|
||||
Assert.Contains("1 transaction", result.Message);
|
||||
}
|
||||
}
|
||||
330
MoneyMap.Tests/Services/ReceiptMatchingServiceTests.cs
Normal file
330
MoneyMap.Tests/Services/ReceiptMatchingServiceTests.cs
Normal file
@@ -0,0 +1,330 @@
|
||||
using MoneyMap.Models;
|
||||
using MoneyMap.Services;
|
||||
using MoneyMap.Tests.TestHelpers;
|
||||
using Xunit;
|
||||
|
||||
namespace MoneyMap.Tests.Services;
|
||||
|
||||
public class ReceiptMatchingServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task GetTransactionIdsWithReceiptsAsync_ReturnsTransactionIds()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new ReceiptMatchingService(context);
|
||||
|
||||
var account = new Account
|
||||
{
|
||||
Id = 1,
|
||||
Institution = "Test Bank",
|
||||
AccountType = AccountType.Checking,
|
||||
Last4 = "1234",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Accounts.Add(account);
|
||||
|
||||
var transaction1 = new Transaction
|
||||
{
|
||||
Id = 1,
|
||||
Date = DateTime.Now,
|
||||
Amount = -50.00m,
|
||||
Name = "Store A",
|
||||
Memo = "Test",
|
||||
AccountId = 1
|
||||
};
|
||||
var transaction2 = new Transaction
|
||||
{
|
||||
Id = 2,
|
||||
Date = DateTime.Now,
|
||||
Amount = -30.00m,
|
||||
Name = "Store B",
|
||||
Memo = "Test",
|
||||
AccountId = 1
|
||||
};
|
||||
context.Transactions.AddRange(transaction1, transaction2);
|
||||
|
||||
var receipt = new Receipt
|
||||
{
|
||||
Id = 1,
|
||||
TransactionId = 1,
|
||||
FileName = "receipt.pdf",
|
||||
ContentType = "application/pdf",
|
||||
StoragePath = "/receipts/receipt.pdf",
|
||||
FileSizeBytes = 1024,
|
||||
FileHashSha256 = "hash123",
|
||||
UploadedAtUtc = DateTime.UtcNow
|
||||
};
|
||||
context.Receipts.Add(receipt);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await service.GetTransactionIdsWithReceiptsAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Contains(1L, result);
|
||||
Assert.DoesNotContain(2L, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindMatchingTransactionsAsync_FiltersByDateRange()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new ReceiptMatchingService(context);
|
||||
|
||||
var account = new Account
|
||||
{
|
||||
Id = 1,
|
||||
Institution = "Test Bank",
|
||||
AccountType = AccountType.Checking,
|
||||
Last4 = "1234",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Accounts.Add(account);
|
||||
|
||||
var receiptDate = new DateTime(2025, 1, 15);
|
||||
|
||||
var withinRange = new Transaction
|
||||
{
|
||||
Id = 1,
|
||||
Date = receiptDate.AddDays(2), // Within +/- 3 days
|
||||
Amount = -50.00m,
|
||||
Name = "Store A",
|
||||
Memo = "Test",
|
||||
AccountId = 1
|
||||
};
|
||||
var outsideRange = new Transaction
|
||||
{
|
||||
Id = 2,
|
||||
Date = receiptDate.AddDays(5), // Outside +/- 3 days
|
||||
Amount = -50.00m,
|
||||
Name = "Store B",
|
||||
Memo = "Test",
|
||||
AccountId = 1
|
||||
};
|
||||
context.Transactions.AddRange(withinRange, outsideRange);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var criteria = new ReceiptMatchCriteria
|
||||
{
|
||||
ReceiptDate = receiptDate,
|
||||
ExcludeTransactionIds = new HashSet<long>()
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await service.FindMatchingTransactionsAsync(criteria);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal(1, result[0].Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindMatchingTransactionsAsync_FiltersByAmountTolerance()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new ReceiptMatchingService(context);
|
||||
|
||||
var account = new Account
|
||||
{
|
||||
Id = 1,
|
||||
Institution = "Test Bank",
|
||||
AccountType = AccountType.Checking,
|
||||
Last4 = "1234",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Accounts.Add(account);
|
||||
|
||||
var receiptDate = DateTime.Now;
|
||||
var receiptTotal = 100.00m;
|
||||
|
||||
var withinTolerance = new Transaction
|
||||
{
|
||||
Id = 1,
|
||||
Date = receiptDate,
|
||||
Amount = -105.00m, // Within 10% tolerance
|
||||
Name = "Store A",
|
||||
Memo = "Test",
|
||||
AccountId = 1
|
||||
};
|
||||
var outsideTolerance = new Transaction
|
||||
{
|
||||
Id = 2,
|
||||
Date = receiptDate,
|
||||
Amount = -150.00m, // Outside 10% tolerance
|
||||
Name = "Store B",
|
||||
Memo = "Test",
|
||||
AccountId = 1
|
||||
};
|
||||
context.Transactions.AddRange(withinTolerance, outsideTolerance);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var criteria = new ReceiptMatchCriteria
|
||||
{
|
||||
ReceiptDate = receiptDate,
|
||||
Total = receiptTotal,
|
||||
ExcludeTransactionIds = new HashSet<long>()
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await service.FindMatchingTransactionsAsync(criteria);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal(1, result[0].Id);
|
||||
Assert.True(result[0].IsCloseAmount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindMatchingTransactionsAsync_MarksExactAmountMatch()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new ReceiptMatchingService(context);
|
||||
|
||||
var account = new Account
|
||||
{
|
||||
Id = 1,
|
||||
Institution = "Test Bank",
|
||||
AccountType = AccountType.Checking,
|
||||
Last4 = "1234",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Accounts.Add(account);
|
||||
|
||||
var receiptDate = DateTime.Now;
|
||||
var receiptTotal = 100.00m;
|
||||
|
||||
var exactMatch = new Transaction
|
||||
{
|
||||
Id = 1,
|
||||
Date = receiptDate,
|
||||
Amount = -100.00m, // Exact match
|
||||
Name = "Store A",
|
||||
Memo = "Test",
|
||||
AccountId = 1
|
||||
};
|
||||
context.Transactions.Add(exactMatch);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var criteria = new ReceiptMatchCriteria
|
||||
{
|
||||
ReceiptDate = receiptDate,
|
||||
Total = receiptTotal,
|
||||
ExcludeTransactionIds = new HashSet<long>()
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await service.FindMatchingTransactionsAsync(criteria);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.True(result[0].IsExactAmount);
|
||||
Assert.False(result[0].IsCloseAmount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindMatchingTransactionsAsync_ExcludesTransactionsWithReceipts()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new ReceiptMatchingService(context);
|
||||
|
||||
var account = new Account
|
||||
{
|
||||
Id = 1,
|
||||
Institution = "Test Bank",
|
||||
AccountType = AccountType.Checking,
|
||||
Last4 = "1234",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Accounts.Add(account);
|
||||
|
||||
var receiptDate = DateTime.Now;
|
||||
|
||||
var transaction1 = new Transaction
|
||||
{
|
||||
Id = 1,
|
||||
Date = receiptDate,
|
||||
Amount = -50.00m,
|
||||
Name = "Store A",
|
||||
Memo = "Test",
|
||||
AccountId = 1
|
||||
};
|
||||
var transaction2 = new Transaction
|
||||
{
|
||||
Id = 2,
|
||||
Date = receiptDate,
|
||||
Amount = -50.00m,
|
||||
Name = "Store B",
|
||||
Memo = "Test",
|
||||
AccountId = 1
|
||||
};
|
||||
context.Transactions.AddRange(transaction1, transaction2);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var criteria = new ReceiptMatchCriteria
|
||||
{
|
||||
ReceiptDate = receiptDate,
|
||||
ExcludeTransactionIds = new HashSet<long> { 1 } // Exclude transaction 1
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await service.FindMatchingTransactionsAsync(criteria);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal(2, result[0].Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindMatchingTransactionsAsync_UsesDueDateForBills()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new ReceiptMatchingService(context);
|
||||
|
||||
var account = new Account
|
||||
{
|
||||
Id = 1,
|
||||
Institution = "Test Bank",
|
||||
AccountType = AccountType.Checking,
|
||||
Last4 = "1234",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Accounts.Add(account);
|
||||
|
||||
var receiptDate = new DateTime(2025, 1, 1);
|
||||
var dueDate = new DateTime(2025, 1, 15);
|
||||
|
||||
// Transaction on due date + 3 days (should match for bills)
|
||||
var transaction = new Transaction
|
||||
{
|
||||
Id = 1,
|
||||
Date = dueDate.AddDays(3),
|
||||
Amount = -50.00m,
|
||||
Name = "Utility Company",
|
||||
Memo = "Test",
|
||||
AccountId = 1
|
||||
};
|
||||
context.Transactions.Add(transaction);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var criteria = new ReceiptMatchCriteria
|
||||
{
|
||||
ReceiptDate = receiptDate,
|
||||
DueDate = dueDate, // Bill with due date
|
||||
ExcludeTransactionIds = new HashSet<long>()
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await service.FindMatchingTransactionsAsync(criteria);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal(1, result[0].Id);
|
||||
}
|
||||
}
|
||||
221
MoneyMap.Tests/Services/ReferenceDataServiceTests.cs
Normal file
221
MoneyMap.Tests/Services/ReferenceDataServiceTests.cs
Normal file
@@ -0,0 +1,221 @@
|
||||
using MoneyMap.Models;
|
||||
using MoneyMap.Services;
|
||||
using MoneyMap.Tests.TestHelpers;
|
||||
using Xunit;
|
||||
|
||||
namespace MoneyMap.Tests.Services;
|
||||
|
||||
public class ReferenceDataServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task GetAvailableCategoriesAsync_ReturnsDistinctSortedCategories()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new ReferenceDataService(context);
|
||||
|
||||
var account = new Account
|
||||
{
|
||||
Id = 1,
|
||||
Institution = "Test Bank",
|
||||
AccountType = AccountType.Checking,
|
||||
Last4 = "1234",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Accounts.Add(account);
|
||||
|
||||
var transaction1 = new Transaction
|
||||
{
|
||||
Date = DateTime.Now,
|
||||
Amount = -50.00m,
|
||||
Name = "Test",
|
||||
Memo = "Test",
|
||||
AccountId = 1,
|
||||
Category = "Groceries"
|
||||
};
|
||||
var transaction2 = new Transaction
|
||||
{
|
||||
Date = DateTime.Now,
|
||||
Amount = -30.00m,
|
||||
Name = "Test",
|
||||
Memo = "Test",
|
||||
AccountId = 1,
|
||||
Category = "Gas"
|
||||
};
|
||||
var transaction3 = new Transaction
|
||||
{
|
||||
Date = DateTime.Now,
|
||||
Amount = -20.00m,
|
||||
Name = "Test",
|
||||
Memo = "Test",
|
||||
AccountId = 1,
|
||||
Category = "Groceries" // Duplicate
|
||||
};
|
||||
context.Transactions.AddRange(transaction1, transaction2, transaction3);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await service.GetAvailableCategoriesAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Contains("Groceries", result);
|
||||
Assert.Contains("Gas", result);
|
||||
Assert.Equal("Gas", result[0]); // Alphabetically sorted
|
||||
Assert.Equal("Groceries", result[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAvailableCategoriesAsync_ExcludesEmptyCategories()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new ReferenceDataService(context);
|
||||
|
||||
var account = new Account
|
||||
{
|
||||
Id = 1,
|
||||
Institution = "Test Bank",
|
||||
AccountType = AccountType.Checking,
|
||||
Last4 = "1234",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Accounts.Add(account);
|
||||
|
||||
var transaction1 = new Transaction
|
||||
{
|
||||
Date = DateTime.Now,
|
||||
Amount = -50.00m,
|
||||
Name = "Test",
|
||||
Memo = "Test",
|
||||
AccountId = 1,
|
||||
Category = "Groceries"
|
||||
};
|
||||
var transaction2 = new Transaction
|
||||
{
|
||||
Date = DateTime.Now,
|
||||
Amount = -30.00m,
|
||||
Name = "Test",
|
||||
Memo = "Test",
|
||||
AccountId = 1,
|
||||
Category = "" // Empty
|
||||
};
|
||||
var transaction3 = new Transaction
|
||||
{
|
||||
Date = DateTime.Now,
|
||||
Amount = -20.00m,
|
||||
Name = "Test",
|
||||
Memo = "Test",
|
||||
AccountId = 1,
|
||||
Category = " " // Whitespace
|
||||
};
|
||||
context.Transactions.AddRange(transaction1, transaction2, transaction3);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await service.GetAvailableCategoriesAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal("Groceries", result[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAvailableMerchantsAsync_ReturnsSortedMerchants()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new ReferenceDataService(context);
|
||||
|
||||
var merchant1 = new Merchant { Name = "Walmart" };
|
||||
var merchant2 = new Merchant { Name = "Amazon" };
|
||||
var merchant3 = new Merchant { Name = "Target" };
|
||||
context.Merchants.AddRange(merchant1, merchant2, merchant3);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await service.GetAvailableMerchantsAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, result.Count);
|
||||
Assert.Equal("Amazon", result[0].Name); // Alphabetically sorted
|
||||
Assert.Equal("Target", result[1].Name);
|
||||
Assert.Equal("Walmart", result[2].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAvailableCardsAsync_ReturnsSortedCards()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new ReferenceDataService(context);
|
||||
|
||||
var account = new Account
|
||||
{
|
||||
Id = 1,
|
||||
Institution = "Test Bank",
|
||||
AccountType = AccountType.Checking,
|
||||
Last4 = "1234",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Accounts.Add(account);
|
||||
|
||||
var card1 = new Card
|
||||
{
|
||||
AccountId = 1,
|
||||
Issuer = "VISA",
|
||||
Last4 = "3333",
|
||||
Owner = "Bob"
|
||||
};
|
||||
var card2 = new Card
|
||||
{
|
||||
AccountId = 1,
|
||||
Issuer = "Mastercard",
|
||||
Last4 = "1111",
|
||||
Owner = "Alice"
|
||||
};
|
||||
context.Cards.AddRange(card1, card2);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await service.GetAvailableCardsAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Equal("Alice", result[0].Owner); // Sorted by owner
|
||||
Assert.Equal("Bob", result[1].Owner);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAvailableAccountsAsync_ReturnsSortedAccounts()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new ReferenceDataService(context);
|
||||
|
||||
var account1 = new Account
|
||||
{
|
||||
Institution = "Wells Fargo",
|
||||
AccountType = AccountType.Checking,
|
||||
Last4 = "5678",
|
||||
Owner = "Test"
|
||||
};
|
||||
var account2 = new Account
|
||||
{
|
||||
Institution = "Bank of America",
|
||||
AccountType = AccountType.Savings,
|
||||
Last4 = "1234",
|
||||
Owner = "Test"
|
||||
};
|
||||
context.Accounts.AddRange(account1, account2);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await service.GetAvailableAccountsAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Equal("Bank of America", result[0].Institution); // Sorted by institution
|
||||
Assert.Equal("Wells Fargo", result[1].Institution);
|
||||
}
|
||||
}
|
||||
229
MoneyMap.Tests/Services/TransactionServiceTests.cs
Normal file
229
MoneyMap.Tests/Services/TransactionServiceTests.cs
Normal file
@@ -0,0 +1,229 @@
|
||||
using MoneyMap.Models;
|
||||
using MoneyMap.Services;
|
||||
using MoneyMap.Tests.TestHelpers;
|
||||
using Xunit;
|
||||
|
||||
namespace MoneyMap.Tests.Services;
|
||||
|
||||
public class TransactionServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task IsDuplicateAsync_ReturnsFalse_WhenTransactionDoesNotExist()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new TransactionService(context);
|
||||
|
||||
var transaction = new Transaction
|
||||
{
|
||||
Date = DateTime.Now,
|
||||
Amount = -50.00m,
|
||||
Name = "Test Store",
|
||||
Memo = "Test purchase",
|
||||
AccountId = 1,
|
||||
CardId = 1
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await service.IsDuplicateAsync(transaction);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IsDuplicateAsync_ReturnsTrue_WhenExactDuplicateExists()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new TransactionService(context);
|
||||
|
||||
// Add an account first (required for transaction)
|
||||
var account = new Account
|
||||
{
|
||||
Id = 1,
|
||||
Institution = "Test Bank",
|
||||
AccountType = AccountType.Checking,
|
||||
Last4 = "1234",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Accounts.Add(account);
|
||||
|
||||
var existingTransaction = new Transaction
|
||||
{
|
||||
Date = new DateTime(2025, 1, 15),
|
||||
Amount = -50.00m,
|
||||
Name = "Test Store",
|
||||
Memo = "Test purchase",
|
||||
AccountId = 1,
|
||||
CardId = 1
|
||||
};
|
||||
context.Transactions.Add(existingTransaction);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var duplicateTransaction = new Transaction
|
||||
{
|
||||
Date = new DateTime(2025, 1, 15),
|
||||
Amount = -50.00m,
|
||||
Name = "Test Store",
|
||||
Memo = "Test purchase",
|
||||
AccountId = 1,
|
||||
CardId = 1
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await service.IsDuplicateAsync(duplicateTransaction);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IsDuplicateAsync_ReturnsFalse_WhenAmountDiffers()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new TransactionService(context);
|
||||
|
||||
var account = new Account
|
||||
{
|
||||
Id = 1,
|
||||
Institution = "Test Bank",
|
||||
AccountType = AccountType.Checking,
|
||||
Last4 = "1234",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Accounts.Add(account);
|
||||
|
||||
var existingTransaction = new Transaction
|
||||
{
|
||||
Date = new DateTime(2025, 1, 15),
|
||||
Amount = -50.00m,
|
||||
Name = "Test Store",
|
||||
Memo = "Test purchase",
|
||||
AccountId = 1,
|
||||
CardId = 1
|
||||
};
|
||||
context.Transactions.Add(existingTransaction);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var differentTransaction = new Transaction
|
||||
{
|
||||
Date = new DateTime(2025, 1, 15),
|
||||
Amount = -51.00m, // Different amount
|
||||
Name = "Test Store",
|
||||
Memo = "Test purchase",
|
||||
AccountId = 1,
|
||||
CardId = 1
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await service.IsDuplicateAsync(differentTransaction);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTransactionByIdAsync_ReturnsNull_WhenTransactionDoesNotExist()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new TransactionService(context);
|
||||
|
||||
// Act
|
||||
var result = await service.GetTransactionByIdAsync(999);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTransactionByIdAsync_ReturnsTransaction_WhenExists()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new TransactionService(context);
|
||||
|
||||
var account = new Account
|
||||
{
|
||||
Id = 1,
|
||||
Institution = "Test Bank",
|
||||
AccountType = AccountType.Checking,
|
||||
Last4 = "1234",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Accounts.Add(account);
|
||||
|
||||
var transaction = new Transaction
|
||||
{
|
||||
Id = 1,
|
||||
Date = DateTime.Now,
|
||||
Amount = -50.00m,
|
||||
Name = "Test Store",
|
||||
Memo = "Test purchase",
|
||||
AccountId = 1
|
||||
};
|
||||
context.Transactions.Add(transaction);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await service.GetTransactionByIdAsync(1);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("Test Store", result.Name);
|
||||
Assert.Equal(-50.00m, result.Amount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteTransactionAsync_ReturnsTrue_WhenTransactionExists()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new TransactionService(context);
|
||||
|
||||
var account = new Account
|
||||
{
|
||||
Id = 1,
|
||||
Institution = "Test Bank",
|
||||
AccountType = AccountType.Checking,
|
||||
Last4 = "1234",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Accounts.Add(account);
|
||||
|
||||
var transaction = new Transaction
|
||||
{
|
||||
Id = 1,
|
||||
Date = DateTime.Now,
|
||||
Amount = -50.00m,
|
||||
Name = "Test Store",
|
||||
Memo = "Test purchase",
|
||||
AccountId = 1
|
||||
};
|
||||
context.Transactions.Add(transaction);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await service.DeleteTransactionAsync(1);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
Assert.Empty(context.Transactions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteTransactionAsync_ReturnsFalse_WhenTransactionDoesNotExist()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new TransactionService(context);
|
||||
|
||||
// Act
|
||||
var result = await service.DeleteTransactionAsync(999);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
}
|
||||
188
MoneyMap.Tests/Services/TransactionStatisticsServiceTests.cs
Normal file
188
MoneyMap.Tests/Services/TransactionStatisticsServiceTests.cs
Normal file
@@ -0,0 +1,188 @@
|
||||
using MoneyMap.Models;
|
||||
using MoneyMap.Services;
|
||||
using MoneyMap.Tests.TestHelpers;
|
||||
using Xunit;
|
||||
|
||||
namespace MoneyMap.Tests.Services;
|
||||
|
||||
public class TransactionStatisticsServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task CalculateStatsAsync_ReturnsCorrectStatistics()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new TransactionStatisticsService(context);
|
||||
|
||||
var account = new Account
|
||||
{
|
||||
Id = 1,
|
||||
Institution = "Test Bank",
|
||||
AccountType = AccountType.Checking,
|
||||
Last4 = "1234",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Accounts.Add(account);
|
||||
|
||||
var debit1 = new Transaction
|
||||
{
|
||||
Date = DateTime.Now,
|
||||
Amount = -50.00m,
|
||||
Name = "Store A",
|
||||
Memo = "Purchase",
|
||||
AccountId = 1
|
||||
};
|
||||
var debit2 = new Transaction
|
||||
{
|
||||
Date = DateTime.Now,
|
||||
Amount = -30.00m,
|
||||
Name = "Store B",
|
||||
Memo = "Purchase",
|
||||
AccountId = 1
|
||||
};
|
||||
var credit = new Transaction
|
||||
{
|
||||
Date = DateTime.Now,
|
||||
Amount = 100.00m,
|
||||
Name = "Deposit",
|
||||
Memo = "Paycheck",
|
||||
AccountId = 1
|
||||
};
|
||||
context.Transactions.AddRange(debit1, debit2, credit);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var query = context.Transactions.AsQueryable();
|
||||
|
||||
// Act
|
||||
var result = await service.CalculateStatsAsync(query);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, result.Count);
|
||||
Assert.Equal(-80.00m, result.TotalDebits);
|
||||
Assert.Equal(100.00m, result.TotalCredits);
|
||||
Assert.Equal(20.00m, result.NetAmount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetCategorizationStatsAsync_ReturnsCorrectCounts()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new TransactionStatisticsService(context);
|
||||
|
||||
var account = new Account
|
||||
{
|
||||
Id = 1,
|
||||
Institution = "Test Bank",
|
||||
AccountType = AccountType.Checking,
|
||||
Last4 = "1234",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Accounts.Add(account);
|
||||
|
||||
var categorized1 = new Transaction
|
||||
{
|
||||
Date = DateTime.Now,
|
||||
Amount = -50.00m,
|
||||
Name = "Test",
|
||||
Memo = "Test",
|
||||
AccountId = 1,
|
||||
Category = "Groceries"
|
||||
};
|
||||
var categorized2 = new Transaction
|
||||
{
|
||||
Date = DateTime.Now,
|
||||
Amount = -30.00m,
|
||||
Name = "Test",
|
||||
Memo = "Test",
|
||||
AccountId = 1,
|
||||
Category = "Gas"
|
||||
};
|
||||
var uncategorized = new Transaction
|
||||
{
|
||||
Date = DateTime.Now,
|
||||
Amount = -20.00m,
|
||||
Name = "Test",
|
||||
Memo = "Test",
|
||||
AccountId = 1,
|
||||
Category = ""
|
||||
};
|
||||
context.Transactions.AddRange(categorized1, categorized2, uncategorized);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await service.GetCategorizationStatsAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, result.TotalTransactions);
|
||||
Assert.Equal(2, result.Categorized);
|
||||
Assert.Equal(1, result.Uncategorized);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetCardStatsForAccountAsync_ReturnsStatsForLinkedCards()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new TransactionStatisticsService(context);
|
||||
|
||||
var account = new Account
|
||||
{
|
||||
Id = 1,
|
||||
Institution = "Test Bank",
|
||||
AccountType = AccountType.Checking,
|
||||
Last4 = "1234",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Accounts.Add(account);
|
||||
|
||||
var card1 = new Card
|
||||
{
|
||||
Id = 1,
|
||||
AccountId = 1,
|
||||
Issuer = "VISA",
|
||||
Last4 = "1111",
|
||||
Owner = "Test"
|
||||
};
|
||||
var card2 = new Card
|
||||
{
|
||||
Id = 2,
|
||||
AccountId = 1,
|
||||
Issuer = "Mastercard",
|
||||
Last4 = "2222",
|
||||
Owner = "Test"
|
||||
};
|
||||
context.Cards.AddRange(card1, card2);
|
||||
|
||||
var transaction1 = new Transaction
|
||||
{
|
||||
Date = DateTime.Now,
|
||||
Amount = -50.00m,
|
||||
Name = "Test",
|
||||
Memo = "Test",
|
||||
AccountId = 1,
|
||||
CardId = 1
|
||||
};
|
||||
var transaction2 = new Transaction
|
||||
{
|
||||
Date = DateTime.Now,
|
||||
Amount = -30.00m,
|
||||
Name = "Test",
|
||||
Memo = "Test",
|
||||
AccountId = 1,
|
||||
CardId = 1
|
||||
};
|
||||
context.Transactions.AddRange(transaction1, transaction2);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await service.GetCardStatsForAccountAsync(1);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, result.Count);
|
||||
var card1Stats = result.First(c => c.Card.Id == 1);
|
||||
Assert.Equal(2, card1Stats.TransactionCount);
|
||||
var card2Stats = result.First(c => c.Card.Id == 2);
|
||||
Assert.Equal(0, card2Stats.TransactionCount);
|
||||
}
|
||||
}
|
||||
20
MoneyMap.Tests/TestHelpers/DbContextHelper.cs
Normal file
20
MoneyMap.Tests/TestHelpers/DbContextHelper.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MoneyMap.Data;
|
||||
|
||||
namespace MoneyMap.Tests.TestHelpers;
|
||||
|
||||
public static class DbContextHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an in-memory database context for testing.
|
||||
/// Each call creates a unique database to ensure test isolation.
|
||||
/// </summary>
|
||||
public static MoneyMapContext CreateInMemoryContext()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<MoneyMapContext>()
|
||||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
|
||||
return new MoneyMapContext(options);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ VisualStudioVersion = 17.14.36429.23
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MoneyMap", "MoneyMap\MoneyMap.csproj", "{B273A467-3592-4675-B1EC-C41C9CE455DB}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MoneyMap.Tests", "MoneyMap.Tests\MoneyMap.Tests.csproj", "{4CAD4283-4E2D-B998-4839-03B72BDDBEF5}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -15,6 +17,10 @@ Global
|
||||
{B273A467-3592-4675-B1EC-C41C9CE455DB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B273A467-3592-4675-B1EC-C41C9CE455DB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B273A467-3592-4675-B1EC-C41C9CE455DB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4CAD4283-4E2D-B998-4839-03B72BDDBEF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4CAD4283-4E2D-B998-4839-03B72BDDBEF5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4CAD4283-4E2D-B998-4839-03B72BDDBEF5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4CAD4283-4E2D-B998-4839-03B72BDDBEF5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
Reference in New Issue
Block a user