测试覆盖率
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 27s
Docker Build & Deploy / Deploy to Production (push) Successful in 9s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 2s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s

This commit is contained in:
SunCheng
2026-01-28 17:00:58 +08:00
parent 3ed9cf5ebd
commit e93c3d6bae
30 changed files with 2492 additions and 227 deletions

View File

@@ -0,0 +1,35 @@
using FluentAssertions;
namespace WebApi.Test.Repository;
public class BudgetArchiveRepositoryTest : RepositoryTestBase
{
private readonly IBudgetArchiveRepository _repository;
public BudgetArchiveRepositoryTest()
{
_repository = new BudgetArchiveRepository(FreeSql);
}
[Fact]
public async Task GetArchiveAsync_获取单条归档_Test()
{
await _repository.AddAsync(new BudgetArchive { Year = 2023, Month = 1 });
await _repository.AddAsync(new BudgetArchive { Year = 2023, Month = 2 });
var archive = await _repository.GetArchiveAsync(2023, 1);
archive.Should().NotBeNull();
archive!.Month.Should().Be(1);
}
[Fact]
public async Task GetArchivesByYearAsync_按年获取_Test()
{
await _repository.AddAsync(new BudgetArchive { Year = 2023, Month = 1 });
await _repository.AddAsync(new BudgetArchive { Year = 2023, Month = 2 });
await _repository.AddAsync(new BudgetArchive { Year = 2022, Month = 12 });
var list = await _repository.GetArchivesByYearAsync(2023);
list.Should().HaveCount(2);
}
}

View File

@@ -0,0 +1,72 @@
using FluentAssertions;
namespace WebApi.Test.Repository;
public class BudgetRepositoryTest : TransactionTestBase
{
private readonly IBudgetRepository _repository;
private readonly ITransactionRecordRepository _transactionRepository;
public BudgetRepositoryTest()
{
_repository = new BudgetRepository(FreeSql);
_transactionRepository = new TransactionRecordRepository(FreeSql);
}
[Fact]
public async Task GetCurrentAmountAsync_计算预算金额_Test()
{
// Arrange
// 插入一些交易记录
await _transactionRepository.AddAsync(CreateExpense(100, classify: "餐饮")); // A
await _transactionRepository.AddAsync(CreateExpense(200, classify: "交通")); // B
await _transactionRepository.AddAsync(CreateExpense(50, classify: "餐饮")); // C
await _transactionRepository.AddAsync(CreateIncome(1000)); // 收入,不应计入支出预算
var budget = new BudgetRecord
{
Limit = 2000,
Category = BudgetCategory.Expense,
SelectedCategories = "餐饮,购物", // Only 餐饮 matches
Name = "日常开销"
};
var startDate = DateTime.Now.AddDays(-1);
var endDate = DateTime.Now.AddDays(1);
// Act
var amount = await _repository.GetCurrentAmountAsync(budget, startDate, endDate);
// Assert
// Should sum A+C = -150. But wait, transaction amounts for expense are negative in CreateExpense?
// Let's check CreateExpense: return CreateTestRecord(-amount, ...);
// So actual stored values are -100, -200, -50.
// SumAsync sums them up. Result should be -150.
amount.Should().Be(-150);
}
[Fact]
public async Task UpdateBudgetCategoryNameAsync_更新分类名称_Test()
{
// Arrange
await _repository.AddAsync(new BudgetRecord { Name = "B1", SelectedCategories = "餐饮,交通", Category = BudgetCategory.Expense });
await _repository.AddAsync(new BudgetRecord { Name = "B2", SelectedCategories = "餐饮", Category = BudgetCategory.Expense });
await _repository.AddAsync(new BudgetRecord { Name = "B3", SelectedCategories = "住宿", Category = BudgetCategory.Expense });
// Act
// 将 "餐饮" 更新为 "美食"
await _repository.UpdateBudgetCategoryNameAsync("餐饮", "美食", TransactionType.Expense);
// Assert
var b1 = await _repository.GetByIdAsync(1); // Assuming ID 1 (Standard FreeSql behavior depending on implementation, but I used standard Add)
// Actually, IDs are snowflake. I should capture them.
var all = await _repository.GetAllAsync();
var b1_updated = all.First(b => b.Name == "B1");
b1_updated.SelectedCategories.Should().Contain("美食");
b1_updated.SelectedCategories.Should().NotContain("餐饮");
var b2_updated = all.First(b => b.Name == "B2");
b2_updated.SelectedCategories.Should().Be("美食");
}
}

View File

@@ -0,0 +1,23 @@
using FluentAssertions;
namespace WebApi.Test.Repository;
public class ConfigRepositoryTest : RepositoryTestBase
{
private readonly IConfigRepository _repository;
public ConfigRepositoryTest()
{
_repository = new ConfigRepository(FreeSql);
}
[Fact]
public async Task GetByKeyAsync_获取配置_Test()
{
await _repository.AddAsync(new ConfigEntity { Key = "k1", Value = "v1" });
var config = await _repository.GetByKeyAsync("k1");
config.Should().NotBeNull();
config!.Value.Should().Be("v1");
}
}

View File

@@ -0,0 +1,54 @@
using FluentAssertions;
namespace WebApi.Test.Repository;
public class EmailMessageRepositoryTest : RepositoryTestBase
{
private readonly IEmailMessageRepository _repository;
public EmailMessageRepositoryTest()
{
_repository = new EmailMessageRepository(FreeSql);
}
[Fact]
public async Task ExistsAsync_检查存在_Test()
{
await _repository.AddAsync(new EmailMessage { Md5 = "md5_value", Subject = "Test" });
var msg = await _repository.ExistsAsync("md5_value");
msg.Should().NotBeNull();
var notfound = await _repository.ExistsAsync("other");
notfound.Should().BeNull();
}
[Fact]
public async Task GetPagedListAsync_游标分页_Test()
{
// 插入3条数据时间倒序
var m1 = new EmailMessage { Subject = "M1", ReceivedDate = DateTime.Now }; // Latest
var m2 = new EmailMessage { Subject = "M2", ReceivedDate = DateTime.Now.AddDays(-1) };
var m3 = new EmailMessage { Subject = "M3", ReceivedDate = DateTime.Now.AddDays(-2) }; // Oldest
// FreeSql IDs are snowflakes, increasing.
// Assuming ID order follows insertion (mostly true for snowflakes if generated sequentially)
// But ReceivedDate is the primary sort in logic usually.
// Let's verify standard cursor pagination usually sorts by Date DESC, ID DESC.
await _repository.AddAsync(m1);
await _repository.AddAsync(m2);
await _repository.AddAsync(m3);
// Fetch page 1 (size 2)
var result1 = await _repository.GetPagedListAsync(null, null, 2);
result1.list.Should().HaveCount(2);
result1.list[0].Subject.Should().Be("M1");
result1.list[1].Subject.Should().Be("M2");
// Fetch page 2 using cursor
var result2 = await _repository.GetPagedListAsync(result1.lastReceivedDate, result1.lastId, 2);
result2.list.Should().HaveCount(1);
result2.list[0].Subject.Should().Be("M3");
}
}

View File

@@ -0,0 +1,39 @@
using FluentAssertions;
namespace WebApi.Test.Repository;
public class MessageRecordRepositoryTest : RepositoryTestBase
{
private readonly IMessageRecordRepository _repository;
public MessageRecordRepositoryTest()
{
_repository = new MessageRecordRepository(FreeSql);
}
[Fact]
public async Task GetPagedListAsync_分页_Test()
{
for (int i = 0; i < 5; i++)
{
await _repository.AddAsync(new MessageRecord { Content = $"Msg{i}" });
}
var result = await _repository.GetPagedListAsync(1, 2);
result.Total.Should().Be(5);
result.List.Should().HaveCount(2);
}
[Fact]
public async Task MarkAllAsReadAsync_全部标记已读_Test()
{
await _repository.AddAsync(new MessageRecord { IsRead = false });
await _repository.AddAsync(new MessageRecord { IsRead = false });
await _repository.AddAsync(new MessageRecord { IsRead = true });
await _repository.MarkAllAsReadAsync();
var all = await _repository.GetAllAsync();
all.All(x => x.IsRead).Should().BeTrue();
}
}

View File

@@ -0,0 +1,23 @@
using FluentAssertions;
namespace WebApi.Test.Repository;
public class PushSubscriptionRepositoryTest : RepositoryTestBase
{
private readonly IPushSubscriptionRepository _repository;
public PushSubscriptionRepositoryTest()
{
_repository = new PushSubscriptionRepository(FreeSql);
}
[Fact]
public async Task GetByEndpointAsync_通过Endpoint获取_Test()
{
await _repository.AddAsync(new PushSubscription { Endpoint = "ep1" });
var sub = await _repository.GetByEndpointAsync("ep1");
sub.Should().NotBeNull();
sub!.Endpoint.Should().Be("ep1");
}
}

View File

@@ -0,0 +1,30 @@
using FreeSql;
using WebApi.Test.Basic;
namespace WebApi.Test.Repository;
public abstract class RepositoryTestBase : BaseTest, IDisposable
{
protected IFreeSql FreeSql { get; }
protected RepositoryTestBase()
{
FreeSql = new FreeSqlBuilder()
.UseConnectionString(DataType.Sqlite, "Data Source=:memory:")
.UseAutoSyncStructure(true)
.UseNoneCommandParameter(true)
.Build();
}
public void Dispose()
{
try
{
FreeSql.Dispose();
}
catch
{
// ignore
}
}
}

View File

@@ -0,0 +1,52 @@
using FluentAssertions;
namespace WebApi.Test.Repository;
public class TransactionCategoryRepositoryTest : TransactionTestBase
{
private readonly ITransactionCategoryRepository _repository;
private readonly ITransactionRecordRepository _transactionRepository;
public TransactionCategoryRepositoryTest()
{
_repository = new TransactionCategoryRepository(FreeSql);
_transactionRepository = new TransactionRecordRepository(FreeSql);
}
[Fact]
public async Task GetCategoriesByTypeAsync_按类型获取_Test()
{
await _repository.AddAsync(new TransactionCategory { Name = "C1", Type = TransactionType.Expense });
await _repository.AddAsync(new TransactionCategory { Name = "C2", Type = TransactionType.Income });
var results = await _repository.GetCategoriesByTypeAsync(TransactionType.Expense);
results.Should().HaveCount(1);
results.First().Name.Should().Be("C1");
}
[Fact]
public async Task GetByNameAndTypeAsync_按名称和类型查找_Test()
{
await _repository.AddAsync(new TransactionCategory { Name = "C1", Type = TransactionType.Expense });
var category = await _repository.GetByNameAndTypeAsync("C1", TransactionType.Expense);
category.Should().NotBeNull();
category!.Name.Should().Be("C1");
}
[Fact]
public async Task IsCategoryInUseAsync_检查是否使用_Test()
{
var category = new TransactionCategory { Name = "UsedCategory", Type = TransactionType.Expense };
await _repository.AddAsync(category);
var unused = new TransactionCategory { Name = "Unused", Type = TransactionType.Expense };
await _repository.AddAsync(unused);
// Add transaction using "UsedCategory"
await _transactionRepository.AddAsync(CreateExpense(100, classify: "UsedCategory"));
(await _repository.IsCategoryInUseAsync(category.Id)).Should().BeTrue();
(await _repository.IsCategoryInUseAsync(unused.Id)).Should().BeFalse();
}
}

View File

@@ -0,0 +1,46 @@
using FluentAssertions;
namespace WebApi.Test.Repository;
public class TransactionPeriodicRepositoryTest : TransactionTestBase
{
private readonly ITransactionPeriodicRepository _repository;
public TransactionPeriodicRepositoryTest()
{
_repository = new TransactionPeriodicRepository(FreeSql);
}
[Fact]
public async Task GetPendingPeriodicBillsAsync_获取待执行账单_Test()
{
// 应该执行的NextExecuteTime <= Now
await _repository.AddAsync(new TransactionPeriodic { Reason = "Bill1", NextExecuteTime = DateTime.Now.AddDays(-1), IsEnabled = true });
// 不该执行的NextExecuteTime > Now
await _repository.AddAsync(new TransactionPeriodic { Reason = "Bill2", NextExecuteTime = DateTime.Now.AddDays(1), IsEnabled = true });
// 不该执行的:未激活
await _repository.AddAsync(new TransactionPeriodic { Reason = "Bill3", NextExecuteTime = DateTime.Now.AddDays(-1), IsEnabled = false });
var results = await _repository.GetPendingPeriodicBillsAsync();
results.Should().HaveCount(1);
results.First().Reason.Should().Be("Bill1");
}
[Fact]
public async Task UpdateExecuteTimeAsync_更新执行时间_Test()
{
var bill = new TransactionPeriodic { Reason = "Bill", NextExecuteTime = DateTime.Now };
await _repository.AddAsync(bill);
var last = DateTime.Now;
var next = DateTime.Now.AddMonths(1);
await _repository.UpdateExecuteTimeAsync(bill.Id, last, next);
var updated = await _repository.GetByIdAsync(bill.Id);
updated!.LastExecuteTime.Should().BeCloseTo(last, TimeSpan.FromSeconds(1));
updated.NextExecuteTime.Should().BeCloseTo(next, TimeSpan.FromSeconds(1));
}
}

View File

@@ -0,0 +1,108 @@
using FluentAssertions;
namespace WebApi.Test.Repository;
public class TransactionRecordRepositoryTest : TransactionTestBase
{
private readonly ITransactionRecordRepository _repository;
public TransactionRecordRepositoryTest()
{
_repository = new TransactionRecordRepository(FreeSql);
}
[Fact]
public async Task AddAsync_添加记录_Test()
{
var record = CreateTestRecord(-100);
var result = await _repository.AddAsync(record);
result.Should().BeTrue();
var dbRecord = await _repository.GetByIdAsync(record.Id);
dbRecord.Should().NotBeNull();
dbRecord!.Amount.Should().Be(-100);
}
[Fact]
public async Task QueryAsync_按类型筛选_Test()
{
await _repository.AddAsync(CreateExpense(100));
await _repository.AddAsync(CreateIncome(200));
var expenses = await _repository.QueryAsync(type: TransactionType.Expense);
expenses.Should().HaveCount(1);
expenses.First().Amount.Should().Be(-100);
var incomes = await _repository.QueryAsync(type: TransactionType.Income);
incomes.Should().HaveCount(1);
incomes.First().Amount.Should().Be(200);
}
[Fact]
public async Task QueryAsync_按通过时间范围筛选_Test()
{
await _repository.AddAsync(CreateExpense(100, new DateTime(2023, 1, 1)));
await _repository.AddAsync(CreateExpense(100, new DateTime(2023, 2, 1)));
await _repository.AddAsync(CreateExpense(100, new DateTime(2023, 3, 1)));
// 查询 1月到2月
var results = await _repository.QueryAsync(
startDate: new DateTime(2023, 1, 1),
endDate: new DateTime(2023, 2, 28)); // Include Feb
results.Should().HaveCount(2);
}
[Fact]
public async Task QueryAsync_按年月筛选_Test()
{
await _repository.AddAsync(CreateExpense(100, new DateTime(2023, 1, 15)));
await _repository.AddAsync(CreateExpense(100, new DateTime(2023, 2, 15)));
var results = await _repository.QueryAsync(year: 2023, month: 1);
results.Should().HaveCount(1);
results.First().OccurredAt.Month.Should().Be(1);
}
[Fact]
public async Task CountAsync_统计数量_Test()
{
await _repository.AddAsync(CreateExpense(100));
await _repository.AddAsync(CreateExpense(200));
await _repository.AddAsync(CreateIncome(3000));
var count = await _repository.CountAsync(type: TransactionType.Expense);
count.Should().Be(2);
}
[Fact]
public async Task GetDistinctClassifyAsync_获取去重分类_Test()
{
await _repository.AddAsync(CreateExpense(100, classify: "餐饮"));
await _repository.AddAsync(CreateExpense(100, classify: "餐饮"));
await _repository.AddAsync(CreateExpense(100, classify: "交通"));
var classifies = await _repository.GetDistinctClassifyAsync();
classifies.Should().HaveCount(2);
classifies.Should().Contain("餐饮");
classifies.Should().Contain("交通");
}
[Fact]
public async Task BatchUpdateByReasonAsync_批量更新_Test()
{
await _repository.AddAsync(CreateExpense(100, reason: "麦当劳", classify: "餐饮"));
await _repository.AddAsync(CreateExpense(100, reason: "麦当劳", classify: "餐饮"));
await _repository.AddAsync(CreateExpense(100, reason: "肯德基", classify: "餐饮"));
// 将所有"麦当劳"改为"快餐"分类,类型保持支出
var count = await _repository.BatchUpdateByReasonAsync("麦当劳", TransactionType.Expense, "快餐");
count.Should().Be(2);
var records = await _repository.QueryAsync(reason: "麦当劳");
records.All(r => r.Classify == "快餐").Should().BeTrue();
var kfc = await _repository.QueryAsync(reason: "肯德基");
kfc.First().Classify.Should().Be("餐饮");
}
}

View File

@@ -0,0 +1,36 @@
namespace WebApi.Test.Repository;
public class TransactionTestBase : RepositoryTestBase
{
protected TransactionRecord CreateTestRecord(
decimal amount,
TransactionType type = TransactionType.Expense,
DateTime? occurredAt = null,
string reason = "测试摘要",
string classify = "测试分类")
{
return new TransactionRecord
{
Amount = amount,
Type = type,
OccurredAt = occurredAt ?? DateTime.Now,
Reason = reason,
Classify = classify,
Card = "1234",
Balance = 1000,
EmailMessageId = 1,
ImportNo = Guid.NewGuid().ToString(),
ImportFrom = "测试"
};
}
protected TransactionRecord CreateExpense(decimal amount, DateTime? occurredAt = null, string reason = "支出", string classify = "餐饮")
{
return CreateTestRecord(-amount, TransactionType.Expense, occurredAt, reason, classify);
}
protected TransactionRecord CreateIncome(decimal amount, DateTime? occurredAt = null, string reason = "收入", string classify = "工资")
{
return CreateTestRecord(amount, TransactionType.Income, occurredAt, reason, classify);
}
}