Files
Telegram-Panel/tests/TelegramPanel.Web.Tests/ScheduledTaskRandomDelayTests.cs
2026-08-26 02:37:03 +08:00

218 lines
8.2 KiB
C#

using Microsoft.AspNetCore.Hosting;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using TelegramPanel.Core.Services;
using TelegramPanel.Data;
using TelegramPanel.Data.Entities;
using TelegramPanel.Data.Repositories;
using TelegramPanel.Web.Services;
using Xunit;
namespace TelegramPanel.Web.Tests;
public sealed class ScheduledTaskRandomDelayTests
{
[Fact]
public async Task ()
{
await using var fixture = await Fixture.CreateAsync(300);
var fromUtc = new DateTime(2026, 8, 20, 10, 15, 0, DateTimeKind.Utc);
var exactNextRunUtc = new DateTime(2026, 8, 20, 11, 0, 0, DateTimeKind.Utc);
await fixture.AddScheduledTaskAsync("0 * * * *", fromUtc);
await fixture.Service.RecalculateNextRunsAsync(fromUtc);
var nextRunAtUtc = await fixture.GetOnlyNextRunAtUtcAsync();
Assert.True(nextRunAtUtc > exactNextRunUtc, $"nextRunAtUtc={nextRunAtUtc:O}");
Assert.True(nextRunAtUtc <= exactNextRunUtc.AddSeconds(300), $"nextRunAtUtc={nextRunAtUtc:O}");
}
[Fact]
public async Task Cron窗口()
{
await using var fixture = await Fixture.CreateAsync(300);
var fromUtc = new DateTime(2026, 8, 20, 10, 15, 0, DateTimeKind.Utc);
var exactNextRunUtc = new DateTime(2026, 8, 20, 10, 16, 0, DateTimeKind.Utc);
await fixture.AddScheduledTaskAsync("* * * * *", fromUtc);
await fixture.Service.RecalculateNextRunsAsync(fromUtc);
var nextRunAtUtc = await fixture.GetOnlyNextRunAtUtcAsync();
Assert.True(nextRunAtUtc > exactNextRunUtc, $"nextRunAtUtc={nextRunAtUtc:O}");
Assert.True(nextRunAtUtc <= exactNextRunUtc.AddSeconds(59), $"nextRunAtUtc={nextRunAtUtc:O}");
}
[Fact]
public async Task Cron时间()
{
await using var fixture = await Fixture.CreateAsync(0);
var fromUtc = new DateTime(2026, 8, 20, 10, 15, 0, DateTimeKind.Utc);
var exactNextRunUtc = new DateTime(2026, 8, 20, 11, 0, 0, DateTimeKind.Utc);
await fixture.AddScheduledTaskAsync("0 * * * *", fromUtc);
await fixture.Service.RecalculateNextRunsAsync(fromUtc);
var nextRunAtUtc = await fixture.GetOnlyNextRunAtUtcAsync();
Assert.Equal(exactNextRunUtc, nextRunAtUtc);
}
[Fact]
public async Task ()
{
await using var fixture = await Fixture.CreateAsync(0);
var scheduleId = await fixture.AddScheduledTaskWithLastRunAsync("pausing");
var error = await Assert.ThrowsAsync<InvalidOperationException>(() => fixture.Service.RunNowAsync(
scheduleId,
"host.system",
"batch"));
Assert.Contains("尚未结束", error.Message);
Assert.Equal(1, await fixture.GetBatchTaskCountAsync());
}
private sealed class Fixture : IAsyncDisposable
{
private readonly SqliteConnection _connection;
private readonly AppDbContext _db;
private Fixture(SqliteConnection connection, AppDbContext db, ScheduledTaskService service)
{
_connection = connection;
_db = db;
Service = service;
}
public ScheduledTaskService Service { get; }
public static async Task<Fixture> CreateAsync(int randomDelaySeconds)
{
var connection = new SqliteConnection("Data Source=:memory:");
await connection.OpenAsync();
var db = new AppDbContext(
new DbContextOptionsBuilder<AppDbContext>()
.UseSqlite(connection)
.Options);
await db.Database.EnsureCreatedAsync();
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["ScheduledTasks:RandomDelaySeconds"] = randomDelaySeconds.ToString()
})
.Build();
var service = CreateService(db, configuration);
return new Fixture(connection, db, service);
}
public async Task AddScheduledTaskAsync(string cronExpression, DateTime timestampUtc)
{
_db.ScheduledTasks.Add(new ScheduledTask
{
Name = "定时测试任务",
TaskType = "account_auto_sync",
Status = ScheduledTaskStatuses.Enabled,
Total = 1,
CronExpression = cronExpression,
CreatedAt = timestampUtc,
UpdatedAt = timestampUtc
});
await _db.SaveChangesAsync();
}
public async Task<DateTime> GetOnlyNextRunAtUtcAsync()
{
_db.ChangeTracker.Clear();
var value = await _db.ScheduledTasks
.AsNoTracking()
.Select(x => x.NextRunAtUtc)
.SingleAsync();
Assert.True(value.HasValue);
return DateTime.SpecifyKind(value.Value, DateTimeKind.Utc);
}
public async Task<int> AddScheduledTaskWithLastRunAsync(string status)
{
var batchTask = new BatchTask
{
TaskType = "account_auto_sync",
Status = status,
Total = 1,
CreatedAt = DateTime.UtcNow
};
_db.BatchTasks.Add(batchTask);
await _db.SaveChangesAsync();
var scheduledTask = new ScheduledTask
{
Name = "停止屏障测试任务",
TaskType = "account_auto_sync",
Status = ScheduledTaskStatuses.Enabled,
Total = 1,
CronExpression = "0 * * * *",
LastBatchTaskId = batchTask.Id,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
};
_db.ScheduledTasks.Add(scheduledTask);
await _db.SaveChangesAsync();
return scheduledTask.Id;
}
public Task<int> GetBatchTaskCountAsync() => _db.BatchTasks.CountAsync();
public async ValueTask DisposeAsync()
{
await _db.DisposeAsync();
await _connection.DisposeAsync();
}
private static ScheduledTaskService CreateService(AppDbContext db, IConfiguration configuration)
{
var batchTasks = new BatchTaskManagementService(
new BatchTaskRepository(db),
configuration,
NullLogger<BatchTaskManagementService>.Instance);
var timeZone = new PanelTimeZoneService(
new TestOptionsMonitor<PanelTimeZoneOptions>(new PanelTimeZoneOptions { TimeZoneId = "UTC" }));
var imageStorage = new ImageAssetStorageService(
configuration,
new TestWebHostEnvironment(),
NullLogger<ImageAssetStorageService>.Instance);
return new ScheduledTaskService(
new ScheduledTaskRepository(db),
new CronExpressionService(),
batchTasks,
timeZone,
imageStorage,
configuration);
}
}
private sealed class TestWebHostEnvironment : IWebHostEnvironment
{
public string ApplicationName { get; set; } = "TelegramPanel.Web.Tests";
public IFileProvider WebRootFileProvider { get; set; } = new NullFileProvider();
public string WebRootPath { get; set; } = string.Empty;
public string EnvironmentName { get; set; } = "Testing";
public string ContentRootPath { get; set; } = Path.GetTempPath();
public IFileProvider ContentRootFileProvider { get; set; } = new NullFileProvider();
}
private sealed class TestOptionsMonitor<T> : IOptionsMonitor<T>
{
public TestOptionsMonitor(T value)
{
CurrentValue = value;
}
public T CurrentValue { get; }
public T Get(string? name) => CurrentValue;
public IDisposable? OnChange(Action<T, string?> listener) => null;
}
}