using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Time.Testing; using StellaOps.Notifier.Worker.Digest; namespace StellaOps.Notifier.Tests.Digest; public class InMemoryDigestSchedulerTests { private readonly FakeTimeProvider _timeProvider; private readonly InMemoryDigestScheduler _scheduler; public InMemoryDigestSchedulerTests() { _timeProvider = new FakeTimeProvider(new DateTimeOffset(2024, 1, 15, 10, 0, 0, TimeSpan.Zero)); _scheduler = new InMemoryDigestScheduler( _timeProvider, NullLogger.Instance); } [Fact] public async Task UpsertScheduleAsync_CreatesNewSchedule() { // Arrange var schedule = CreateTestSchedule("schedule-1"); // Act var result = await _scheduler.UpsertScheduleAsync(schedule); // Assert Assert.NotNull(result); Assert.Equal("schedule-1", result.ScheduleId); Assert.NotNull(result.NextRunAt); } [Fact] public async Task UpsertScheduleAsync_UpdatesExistingSchedule() { // Arrange var schedule = CreateTestSchedule("schedule-1"); await _scheduler.UpsertScheduleAsync(schedule); var updated = schedule with { Name = "Updated Name" }; // Act var result = await _scheduler.UpsertScheduleAsync(updated); // Assert Assert.Equal("Updated Name", result.Name); } [Fact] public async Task GetScheduleAsync_ReturnsSchedule() { // Arrange var schedule = CreateTestSchedule("schedule-1"); await _scheduler.UpsertScheduleAsync(schedule); // Act var result = await _scheduler.GetScheduleAsync("tenant1", "schedule-1"); // Assert Assert.NotNull(result); Assert.Equal("schedule-1", result.ScheduleId); } [Fact] public async Task GetScheduleAsync_ReturnsNullForUnknown() { // Act var result = await _scheduler.GetScheduleAsync("tenant1", "unknown"); // Assert Assert.Null(result); } [Fact] public async Task GetSchedulesAsync_ReturnsTenantSchedules() { // Arrange await _scheduler.UpsertScheduleAsync(CreateTestSchedule("schedule-1", "tenant1")); await _scheduler.UpsertScheduleAsync(CreateTestSchedule("schedule-2", "tenant1")); await _scheduler.UpsertScheduleAsync(CreateTestSchedule("schedule-3", "tenant2")); // Act var result = await _scheduler.GetSchedulesAsync("tenant1"); // Assert Assert.Equal(2, result.Count); Assert.All(result, s => Assert.Equal("tenant1", s.TenantId)); } [Fact] public async Task DeleteScheduleAsync_RemovesSchedule() { // Arrange await _scheduler.UpsertScheduleAsync(CreateTestSchedule("schedule-1")); // Act var deleted = await _scheduler.DeleteScheduleAsync("tenant1", "schedule-1"); // Assert Assert.True(deleted); var result = await _scheduler.GetScheduleAsync("tenant1", "schedule-1"); Assert.Null(result); } [Fact] public async Task DeleteScheduleAsync_ReturnsFalseForUnknown() { // Act var deleted = await _scheduler.DeleteScheduleAsync("tenant1", "unknown"); // Assert Assert.False(deleted); } [Fact] public async Task GetDueSchedulesAsync_ReturnsDueSchedules() { // Arrange - create a schedule that should run every minute var schedule = CreateTestSchedule("schedule-1") with { CronExpression = "0 * * * * *" // Every minute }; await _scheduler.UpsertScheduleAsync(schedule); // Advance time past next run _timeProvider.Advance(TimeSpan.FromMinutes(2)); // Act var dueSchedules = await _scheduler.GetDueSchedulesAsync(_timeProvider.GetUtcNow()); // Assert Assert.Single(dueSchedules); Assert.Equal("schedule-1", dueSchedules[0].ScheduleId); } [Fact] public async Task GetDueSchedulesAsync_ExcludesDisabledSchedules() { // Arrange var schedule = CreateTestSchedule("schedule-1") with { Enabled = false, CronExpression = "0 * * * * *" }; await _scheduler.UpsertScheduleAsync(schedule); _timeProvider.Advance(TimeSpan.FromMinutes(2)); // Act var dueSchedules = await _scheduler.GetDueSchedulesAsync(_timeProvider.GetUtcNow()); // Assert Assert.Empty(dueSchedules); } [Fact] public async Task UpdateLastRunAsync_UpdatesTimestamps() { // Arrange var schedule = CreateTestSchedule("schedule-1") with { CronExpression = "0 0 * * * *" // Every hour }; await _scheduler.UpsertScheduleAsync(schedule); var runTime = _timeProvider.GetUtcNow(); // Act await _scheduler.UpdateLastRunAsync("tenant1", "schedule-1", runTime); // Assert var updated = await _scheduler.GetScheduleAsync("tenant1", "schedule-1"); Assert.NotNull(updated); Assert.Equal(runTime, updated.LastRunAt); Assert.NotNull(updated.NextRunAt); Assert.True(updated.NextRunAt > runTime); } [Fact] public async Task UpsertScheduleAsync_CalculatesNextRunWithTimezone() { // Arrange var schedule = CreateTestSchedule("schedule-1") with { CronExpression = "0 0 9 * * *", // 9 AM every day Timezone = "America/New_York" }; // Act var result = await _scheduler.UpsertScheduleAsync(schedule); // Assert Assert.NotNull(result.NextRunAt); } [Fact] public async Task UpsertScheduleAsync_HandlesInvalidCron() { // Arrange var schedule = CreateTestSchedule("schedule-1") with { CronExpression = "invalid-cron" }; // Act var result = await _scheduler.UpsertScheduleAsync(schedule); // Assert Assert.Null(result.NextRunAt); } [Fact] public async Task GetSchedulesAsync_OrdersByName() { // Arrange await _scheduler.UpsertScheduleAsync(CreateTestSchedule("schedule-c") with { Name = "Charlie" }); await _scheduler.UpsertScheduleAsync(CreateTestSchedule("schedule-a") with { Name = "Alpha" }); await _scheduler.UpsertScheduleAsync(CreateTestSchedule("schedule-b") with { Name = "Bravo" }); // Act var result = await _scheduler.GetSchedulesAsync("tenant1"); // Assert Assert.Equal(3, result.Count); Assert.Equal("Alpha", result[0].Name); Assert.Equal("Bravo", result[1].Name); Assert.Equal("Charlie", result[2].Name); } private DigestSchedule CreateTestSchedule(string id, string tenantId = "tenant1") { return new DigestSchedule { ScheduleId = id, TenantId = tenantId, Name = $"Test Schedule {id}", Enabled = true, CronExpression = "0 0 8 * * *", // 8 AM daily DigestType = DigestType.Daily, Format = DigestFormat.Html, CreatedAt = _timeProvider.GetUtcNow(), Recipients = [ new DigestRecipient { Type = "email", Address = "test@example.com" } ] }; } }