Files
EmailBill/WebApi.Test/Transaction/TransactionPeriodicServiceTest.cs
SunCheng e93c3d6bae
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
测试覆盖率
2026-01-28 17:00:58 +08:00

609 lines
21 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Microsoft.Extensions.Logging;
using Service.Transaction;
namespace WebApi.Test.Transaction;
public class TransactionPeriodicServiceTest : BaseTest
{
private readonly ITransactionPeriodicRepository _periodicRepository = Substitute.For<ITransactionPeriodicRepository>();
private readonly ITransactionRecordRepository _transactionRepository = Substitute.For<ITransactionRecordRepository>();
private readonly IMessageRecordRepository _messageRepository = Substitute.For<IMessageRecordRepository>();
private readonly ILogger<TransactionPeriodicService> _logger = Substitute.For<ILogger<TransactionPeriodicService>>();
private readonly ITransactionPeriodicService _service;
public TransactionPeriodicServiceTest()
{
_service = new TransactionPeriodicService(
_periodicRepository,
_transactionRepository,
_messageRepository,
_logger
);
}
[Fact]
public async Task ExecutePeriodicBillsAsync_每日账单()
{
// Arrange
var today = new DateTime(2024, 1, 15, 10, 0, 0);
var periodicBill = new TransactionPeriodic
{
Id = 1,
PeriodicType = PeriodicType.Daily,
Amount = 100m,
Type = TransactionType.Expense,
Classify = "餐饮",
Reason = "每日餐费",
IsEnabled = true,
LastExecuteTime = new DateTime(2024, 1, 14, 10, 0, 0),
NextExecuteTime = new DateTime(2024, 1, 15, 10, 0, 0)
};
_periodicRepository.GetPendingPeriodicBillsAsync().Returns(new[] { periodicBill });
_transactionRepository.AddAsync(Arg.Any<TransactionRecord>()).Returns(Task.FromResult(true));
_messageRepository.AddAsync(Arg.Any<MessageRecord>()).Returns(Task.CompletedTask);
_periodicRepository.UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
.Returns(Task.CompletedTask);
// Act
await _service.ExecutePeriodicBillsAsync();
// Assert
await _transactionRepository.Received(1).AddAsync(Arg.Is<TransactionRecord>(t =>
t.Amount == -100m &&
t.Type == TransactionType.Expense &&
t.Classify == "餐饮" &&
t.Reason == "每日餐费" &&
t.Card == "周期性账单" &&
t.ImportFrom == "周期性账单自动生成"
));
await _messageRepository.Received(1).AddAsync(Arg.Is<MessageRecord>(m =>
m.Title == "周期性账单提醒" &&
m.Content.Contains("支出") &&
m.Content.Contains("100.00") &&
m.Content.Contains("每日餐费") &&
m.IsRead == false
));
await _periodicRepository.Received(1).UpdateExecuteTimeAsync(
Arg.Is(1L),
Arg.Is<DateTime>(dt => dt.Date == today.Date),
Arg.Is<DateTime?>(dt => dt.HasValue && dt.Value.Date == today.Date.AddDays(1))
);
}
[Fact]
public async Task ExecutePeriodicBillsAsync_每周账单()
{
// Arrange
var periodicBill = new TransactionPeriodic
{
Id = 1,
PeriodicType = PeriodicType.Weekly,
PeriodicConfig = "1,3,5", // 周一、三、五
Amount = 200m,
Type = TransactionType.Expense,
Classify = "交通",
Reason = "每周通勤费",
IsEnabled = true,
LastExecuteTime = new DateTime(2024, 1, 10, 10, 0, 0),
NextExecuteTime = new DateTime(2024, 1, 15, 10, 0, 0)
};
_periodicRepository.GetPendingPeriodicBillsAsync().Returns(new[] { periodicBill });
_transactionRepository.AddAsync(Arg.Any<TransactionRecord>()).Returns(Task.FromResult(true));
_messageRepository.AddAsync(Arg.Any<MessageRecord>()).Returns(Task.CompletedTask);
_periodicRepository.UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
.Returns(Task.CompletedTask);
// Act
await _service.ExecutePeriodicBillsAsync();
// Assert
await _transactionRepository.Received(1).AddAsync(Arg.Is<TransactionRecord>(t =>
t.Amount == -200m &&
t.Type == TransactionType.Expense &&
t.Classify == "交通" &&
t.Reason == "每周通勤费"
));
}
[Fact]
public async Task ExecutePeriodicBillsAsync_每月账单()
{
// Arrange
var periodicBill = new TransactionPeriodic
{
Id = 1,
PeriodicType = PeriodicType.Monthly,
PeriodicConfig = "1,15", // 每月1号和15号
Amount = 5000m,
Type = TransactionType.Income,
Classify = "工资",
Reason = "每月工资",
IsEnabled = true,
LastExecuteTime = new DateTime(2024, 1, 1, 10, 0, 0),
NextExecuteTime = new DateTime(2024, 1, 15, 10, 0, 0)
};
_periodicRepository.GetPendingPeriodicBillsAsync().Returns(new[] { periodicBill });
_transactionRepository.AddAsync(Arg.Any<TransactionRecord>()).Returns(Task.FromResult(true));
_messageRepository.AddAsync(Arg.Any<MessageRecord>()).Returns(Task.CompletedTask);
_periodicRepository.UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
.Returns(Task.CompletedTask);
// Act
await _service.ExecutePeriodicBillsAsync();
// Assert
await _transactionRepository.Received(1).AddAsync(Arg.Is<TransactionRecord>(t =>
t.Amount == 5000m &&
t.Type == TransactionType.Income &&
t.Classify == "工资" &&
t.Reason == "每月工资"
));
await _messageRepository.Received(1).AddAsync(Arg.Is<MessageRecord>(m =>
m.Content.Contains("收入") &&
m.Content.Contains("5000.00") &&
m.Content.Contains("每月工资")
));
}
[Fact]
public async Task ExecutePeriodicBillsAsync_未达到执行时间()
{
// Arrange
var periodicBill = new TransactionPeriodic
{
Id = 1,
PeriodicType = PeriodicType.Weekly,
PeriodicConfig = "1,3,5", // 只在周一(1)、三(3)、五(5)执行
Amount = 200m,
Type = TransactionType.Expense,
Classify = "交通",
Reason = "每周通勤费",
IsEnabled = true,
LastExecuteTime = new DateTime(2024, 1, 10, 10, 0, 0),
NextExecuteTime = new DateTime(2024, 1, 15, 10, 0, 0)
};
_periodicRepository.GetPendingPeriodicBillsAsync().Returns(new[] { periodicBill });
// Act
await _service.ExecutePeriodicBillsAsync();
// Assert
await _transactionRepository.Received(0).AddAsync(Arg.Any<TransactionRecord>());
await _messageRepository.Received(0).AddAsync(Arg.Any<MessageRecord>());
await _periodicRepository.Received(0).UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>());
}
[Fact]
public async Task ExecutePeriodicBillsAsync_今天已执行过()
{
// Arrange
var periodicBill = new TransactionPeriodic
{
Id = 1,
PeriodicType = PeriodicType.Daily,
Amount = 100m,
Type = TransactionType.Expense,
Classify = "餐饮",
Reason = "每日餐费",
IsEnabled = true,
LastExecuteTime = new DateTime(2024, 1, 15, 8, 0, 0), // 今天已经执行过
NextExecuteTime = new DateTime(2024, 1, 16, 10, 0, 0)
};
_periodicRepository.GetPendingPeriodicBillsAsync().Returns(new[] { periodicBill });
// Act
await _service.ExecutePeriodicBillsAsync();
// Assert
// 由于 LastExecuteTime 日期是今天,所以不会再次执行
await _transactionRepository.Received(0).AddAsync(Arg.Any<TransactionRecord>());
await _messageRepository.Received(0).AddAsync(Arg.Any<MessageRecord>());
await _periodicRepository.Received(0).UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>());
}
[Fact]
public async Task ExecutePeriodicBillsAsync_从未执行过()
{
// Arrange
var periodicBill = new TransactionPeriodic
{
Id = 1,
PeriodicType = PeriodicType.Weekly,
PeriodicConfig = "1,3,5",
Amount = 200m,
Type = TransactionType.Expense,
Classify = "交通",
Reason = "每周通勤费",
IsEnabled = true,
LastExecuteTime = null, // 从未执行过
NextExecuteTime = null
};
_periodicRepository.GetPendingPeriodicBillsAsync().Returns(new[] { periodicBill });
_transactionRepository.AddAsync(Arg.Any<TransactionRecord>()).Returns(Task.FromResult(true));
_messageRepository.AddAsync(Arg.Any<MessageRecord>()).Returns(Task.CompletedTask);
_periodicRepository.UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
.Returns(Task.CompletedTask);
// Act
await _service.ExecutePeriodicBillsAsync();
// Assert
await _transactionRepository.Received(1).AddAsync(Arg.Any<TransactionRecord>());
}
[Fact]
public async Task ExecutePeriodicBillsAsync_添加交易记录失败()
{
// Arrange
var periodicBill = new TransactionPeriodic
{
Id = 1,
PeriodicType = PeriodicType.Daily,
Amount = 100m,
Type = TransactionType.Expense,
Classify = "餐饮",
Reason = "每日餐费",
IsEnabled = true,
LastExecuteTime = new DateTime(2024, 1, 14, 10, 0, 0),
NextExecuteTime = new DateTime(2024, 1, 15, 10, 0, 0)
};
_periodicRepository.GetPendingPeriodicBillsAsync().Returns(new[] { periodicBill });
_transactionRepository.AddAsync(Arg.Any<TransactionRecord>()).Returns(false); // 添加失败
// Act
await _service.ExecutePeriodicBillsAsync();
// Assert
await _messageRepository.Received(0).AddAsync(Arg.Any<MessageRecord>());
await _periodicRepository.Received(0).UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>());
}
[Fact]
public async Task ExecutePeriodicBillsAsync_多条账单()
{
// Arrange
var periodicBills = new[]
{
new TransactionPeriodic
{
Id = 1,
PeriodicType = PeriodicType.Daily,
Amount = 100m,
Type = TransactionType.Expense,
Classify = "餐饮",
Reason = "每日餐费",
IsEnabled = true,
LastExecuteTime = new DateTime(2024, 1, 14, 10, 0, 0),
NextExecuteTime = new DateTime(2024, 1, 15, 10, 0, 0)
},
new TransactionPeriodic
{
Id = 2,
PeriodicType = PeriodicType.Daily,
Amount = 200m,
Type = TransactionType.Expense,
Classify = "交通",
Reason = "每日交通",
IsEnabled = true,
LastExecuteTime = new DateTime(2024, 1, 14, 10, 0, 0),
NextExecuteTime = new DateTime(2024, 1, 15, 10, 0, 0)
}
};
_periodicRepository.GetPendingPeriodicBillsAsync().Returns(periodicBills);
_transactionRepository.AddAsync(Arg.Any<TransactionRecord>()).Returns(Task.FromResult(true));
_messageRepository.AddAsync(Arg.Any<MessageRecord>()).Returns(Task.CompletedTask);
_periodicRepository.UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
.Returns(Task.CompletedTask);
// Act
await _service.ExecutePeriodicBillsAsync();
// Assert
await _transactionRepository.Received(2).AddAsync(Arg.Any<TransactionRecord>());
await _messageRepository.Received(2).AddAsync(Arg.Any<MessageRecord>());
await _periodicRepository.Received(2).UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>());
}
[Fact]
public void CalculateNextExecuteTime_每日()
{
// Arrange
var periodic = new TransactionPeriodic
{
Id = 1,
PeriodicType = PeriodicType.Daily,
PeriodicConfig = ""
};
var baseTime = new DateTime(2024, 1, 15, 10, 0, 0);
// Act
var result = _service.CalculateNextExecuteTime(periodic, baseTime);
// Assert
result.Should().Be(new DateTime(2024, 1, 16, 0, 0, 0));
}
[Fact]
public void CalculateNextExecuteTime_每周_本周内()
{
// Arrange
var periodic = new TransactionPeriodic
{
Id = 1,
PeriodicType = PeriodicType.Weekly,
PeriodicConfig = "1,3,5" // 周一、三、五
};
var baseTime = new DateTime(2024, 1, 15, 10, 0, 0); // 周一
// Act
var result = _service.CalculateNextExecuteTime(periodic, baseTime);
// Assert
result.Should().Be(new DateTime(2024, 1, 17, 0, 0, 0)); // 下周三
}
[Fact]
public void CalculateNextExecuteTime_每周_跨周()
{
// Arrange
var periodic = new TransactionPeriodic
{
Id = 1,
PeriodicType = PeriodicType.Weekly,
PeriodicConfig = "1,3,5" // 周一、三、五
};
var baseTime = new DateTime(2024, 1, 19, 10, 0, 0); // 周五
// Act
var result = _service.CalculateNextExecuteTime(periodic, baseTime);
// Assert
result.Should().Be(new DateTime(2024, 1, 22, 0, 0, 0)); // 下周一
}
[Fact]
public void CalculateNextExecuteTime_每月_本月内()
{
// Arrange
var periodic = new TransactionPeriodic
{
Id = 1,
PeriodicType = PeriodicType.Monthly,
PeriodicConfig = "1,15" // 每月1号和15号
};
var baseTime = new DateTime(2024, 1, 10, 10, 0, 0);
// Act
var result = _service.CalculateNextExecuteTime(periodic, baseTime);
// Assert
result.Should().Be(new DateTime(2024, 1, 15, 0, 0, 0));
}
[Fact]
public void CalculateNextExecuteTime_每月_跨月()
{
// Arrange
var periodic = new TransactionPeriodic
{
Id = 1,
PeriodicType = PeriodicType.Monthly,
PeriodicConfig = "1,15" // 每月1号和15号
};
var baseTime = new DateTime(2024, 1, 16, 10, 0, 0);
// Act
var result = _service.CalculateNextExecuteTime(periodic, baseTime);
// Assert
result.Should().Be(new DateTime(2024, 2, 1, 0, 0, 0));
}
[Fact]
public void CalculateNextExecuteTime_每月_月末()
{
// Arrange
var periodic = new TransactionPeriodic
{
Id = 1,
PeriodicType = PeriodicType.Monthly,
PeriodicConfig = "30,31" // 每月30号和31号
};
var baseTime = new DateTime(2024, 1, 15, 10, 0, 0); // 1月只有31天
// Act
var result = _service.CalculateNextExecuteTime(periodic, baseTime);
// Assert
result.Should().Be(new DateTime(2024, 1, 30, 0, 0, 0));
}
[Fact]
public void CalculateNextExecuteTime_每月_小月()
{
// Arrange
var periodic = new TransactionPeriodic
{
Id = 1,
PeriodicType = PeriodicType.Monthly,
PeriodicConfig = "30,31" // 每月30号和31号
};
var baseTime = new DateTime(2024, 4, 25, 10, 0, 0); // 4月只有30天
// Act
var result = _service.CalculateNextExecuteTime(periodic, baseTime);
// Assert
result.Should().Be(new DateTime(2024, 4, 30, 0, 0, 0)); // 30号31号不存在
}
[Fact]
public void CalculateNextExecuteTime_每季度()
{
// Arrange
var periodic = new TransactionPeriodic
{
Id = 1,
PeriodicType = PeriodicType.Quarterly,
PeriodicConfig = "15" // 每季度第15天
};
var baseTime = new DateTime(2024, 1, 15, 10, 0, 0);
// Act
var result = _service.CalculateNextExecuteTime(periodic, baseTime);
// Assert
result.Should().Be(new DateTime(2024, 4, 15, 0, 0, 0)); // 下季度
}
[Fact]
public void CalculateNextExecuteTime_每年()
{
// Arrange
var periodic = new TransactionPeriodic
{
Id = 1,
PeriodicType = PeriodicType.Yearly,
PeriodicConfig = "100" // 每年第100天
};
var baseTime = new DateTime(2024, 4, 10, 10, 0, 0); // 第100天是4月9日
// Act
var result = _service.CalculateNextExecuteTime(periodic, baseTime);
// Assert
result.Should().Be(new DateTime(2025, 4, 9, 0, 0, 0)); // 下一年
}
[Fact]
public void CalculateNextExecuteTime_未知周期类型()
{
// Arrange
var periodic = new TransactionPeriodic
{
Id = 1,
PeriodicType = (PeriodicType)99, // 未知类型
PeriodicConfig = ""
};
var baseTime = new DateTime(2024, 1, 15, 10, 0, 0);
// Act
var result = _service.CalculateNextExecuteTime(periodic, baseTime);
// Assert
result.Should().BeNull();
}
[Fact]
public async Task ExecutePeriodicBillsAsync_处理异常不中断()
{
// Arrange
var periodicBills = new[]
{
new TransactionPeriodic
{
Id = 1,
PeriodicType = PeriodicType.Daily,
Amount = 100m,
Type = TransactionType.Expense,
Classify = "餐饮",
Reason = "每日餐费",
IsEnabled = true,
LastExecuteTime = new DateTime(2024, 1, 14, 10, 0, 0),
NextExecuteTime = new DateTime(2024, 1, 15, 10, 0, 0)
},
new TransactionPeriodic
{
Id = 2,
PeriodicType = PeriodicType.Daily,
Amount = 200m,
Type = TransactionType.Expense,
Classify = "交通",
Reason = "每日交通",
IsEnabled = true,
LastExecuteTime = new DateTime(2024, 1, 14, 10, 0, 0),
NextExecuteTime = new DateTime(2024, 1, 15, 10, 0, 0)
}
};
_periodicRepository.GetPendingPeriodicBillsAsync().Returns(periodicBills);
_transactionRepository.AddAsync(Arg.Any<TransactionRecord>()).Returns(Task.FromResult(true));
_messageRepository.AddAsync(Arg.Any<MessageRecord>()).Returns(Task.CompletedTask);
_periodicRepository.UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
.Returns(args =>
{
var id = (long)args[0];
if (id == 1)
{
throw new Exception("更新失败");
}
return Task.CompletedTask;
});
// Act
await _service.ExecutePeriodicBillsAsync();
// Assert
// 第二条记录应该成功处理
await _transactionRepository.Received(2).AddAsync(Arg.Any<TransactionRecord>());
}
[Fact]
public async Task ExecutePeriodicBillsAsync_处理所有账单()
{
// Arrange
var periodicBills = new[]
{
new TransactionPeriodic
{
Id = 1,
PeriodicType = PeriodicType.Daily,
Amount = 100m,
Type = TransactionType.Expense,
Classify = "餐饮",
Reason = "每日餐费",
IsEnabled = false, // 禁用
LastExecuteTime = new DateTime(2024, 1, 14, 10, 0, 0),
NextExecuteTime = new DateTime(2024, 1, 15, 10, 0, 0)
},
new TransactionPeriodic
{
Id = 2,
PeriodicType = PeriodicType.Daily,
Amount = 200m,
Type = TransactionType.Expense,
Classify = "交通",
Reason = "每日交通",
IsEnabled = true,
LastExecuteTime = new DateTime(2024, 1, 14, 10, 0, 0),
NextExecuteTime = new DateTime(2024, 1, 15, 10, 0, 0)
}
};
_periodicRepository.GetPendingPeriodicBillsAsync().Returns(periodicBills);
_transactionRepository.AddAsync(Arg.Any<TransactionRecord>()).Returns(Task.FromResult(true));
_messageRepository.AddAsync(Arg.Any<MessageRecord>()).Returns(Task.CompletedTask);
_periodicRepository.UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
.Returns(Task.CompletedTask);
// Act
await _service.ExecutePeriodicBillsAsync();
// Assert
// 只有启用的账单会被处理
await _transactionRepository.Received(1).AddAsync(Arg.Is<TransactionRecord>(t => t.Reason == "每日交通"));
await _transactionRepository.Received(0).AddAsync(Arg.Is<TransactionRecord>(t => t.Reason == "每日餐费"));
}
}