w4c-workflows-api/w4c-workflows-api.Tests/CronExpressionTests.cs

89 lines
3.6 KiB
C#

using w4c_workflows.Services.Triggers;
using Xunit;
namespace w4c_workflows.Tests;
public class CronExpressionTests
{
private static DateTimeOffset Utc(int year, int month, int day, int hour, int minute)
=> new(year, month, day, hour, minute, 0, TimeSpan.Zero);
[Theory]
[InlineData("* * * * *", 2026, 8, 28, 14, 21, true)]
[InlineData("*/15 * * * *", 2026, 8, 28, 14, 0, true)]
[InlineData("*/15 * * * *", 2026, 8, 28, 14, 1, false)]
[InlineData("0 */6 * * *", 2026, 8, 28, 6, 0, true)]
[InlineData("0 */6 * * *", 2026, 8, 28, 12, 0, true)]
[InlineData("0 */6 * * *", 2026, 8, 28, 13, 0, false)]
[InlineData("0 9 * * 1-5", 2026, 8, 28, 9, 0, true)] // Friday
[InlineData("0 9 * * 1-5", 2026, 8, 29, 9, 0, false)] // Saturday
[InlineData("0 0 1 * *", 2026, 8, 1, 0, 0, true)]
[InlineData("0 0 1 * *", 2026, 8, 2, 0, 0, false)]
[InlineData("0 9 * * 0", 2026, 8, 30, 9, 0, true)] // Sunday via 0
[InlineData("0 9 * * 7", 2026, 8, 30, 9, 0, true)] // Sunday via alias 7
[InlineData("30 4,16 * * *", 2026, 8, 28, 16, 30, true)]
[InlineData("30 4,16 * * *", 2026, 8, 28, 12, 30, false)]
[InlineData("15-30 * * * *", 2026, 8, 28, 14, 22, true)]
[InlineData("15-30 * * * *", 2026, 8, 28, 14, 31, false)]
public void Matches_standard_5_field_expressions(
string expr, int y, int m, int d, int h, int min, bool expected)
{
Assert.True(CronExpression.TryParse(expr, out var cron, out var error), error);
Assert.Equal(expected, cron!.Matches(new DateTime(y, m, d, h, min, 0, DateTimeKind.Utc)));
}
[Fact]
public void GetNextOccurrence_returns_next_matching_minute()
{
Assert.True(CronExpression.TryParse("0 9 * * *", out var cron, out _));
var next = cron!.GetNextOccurrence(Utc(2026, 8, 28, 10, 0));
Assert.Equal(Utc(2026, 8, 29, 9, 0), next);
}
[Fact]
public void GetNextOccurrence_skips_to_next_day_for_weekday_schedule()
{
Assert.True(CronExpression.TryParse("0 9 * * 1-5", out var cron, out _));
// Friday 10:00 → next is Monday 09:00.
var next = cron!.GetNextOccurrence(Utc(2026, 8, 28, 10, 0));
Assert.Equal(Utc(2026, 8, 31, 9, 0), next);
}
[Fact]
public void GetNextOccurrence_is_strictly_after_the_given_instant()
{
Assert.True(CronExpression.TryParse("* * * * *", out var cron, out _));
var after = Utc(2026, 8, 28, 14, 0);
var next = cron!.GetNextOccurrence(after);
Assert.Equal(Utc(2026, 8, 28, 14, 1), next);
}
[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData("0 9 * *")] // 4 fields
[InlineData("0 9 * * * *")] // 6 fields
[InlineData("60 * * * *")] // minute out of range
[InlineData("0 24 * * *")] // hour out of range
[InlineData("0 9 32 * *")] // day-of-month out of range
[InlineData("0 9 * 13 *")] // month out of range
[InlineData("0 9 * * 8")] // day-of-week out of range
[InlineData("0 9 * * *")] // valid — ensure it is NOT rejected
public void TryParse_validates_field_count_and_ranges(string expr)
{
var result = CronExpression.TryParse(expr, out var cron, out _);
if (expr == "0 9 * * *")
Assert.True(result);
else
Assert.False(result);
}
[Fact]
public void GetNextOccurrence_returns_null_for_unmatchable_schedule()
{
// Feb 30 never occurs — no match within the bounded horizon.
Assert.True(CronExpression.TryParse("0 0 30 2 *", out var cron, out _));
Assert.Null(cron!.GetNextOccurrence(Utc(2026, 8, 28, 0, 0)));
}
}