New config-driven service that replaces JstPeriod. All timestamps are UTC; one config value (GameCalendarConfig.DailyResetUtcHour, default 0 = midnight UTC) drives both ResetReady checks and DayKey/WeekKey/MonthKey bucket keys. Because both derive from the same MostRecentBoundary helper, they can never disagree. Prod parity is a config flip: set DailyResetUtcHour=17 in GameConfigs to get the 02:00 JST boundary. No JST-flavored math anywhere in the code. JstPeriod callsite migration + deletion follow in subsequent commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
129 lines
4.8 KiB
C#
129 lines
4.8 KiB
C#
using SVSim.Database.Models.Config;
|
|
using SVSim.Database.Services;
|
|
using SVSim.EmulatedEntrypoint.Services;
|
|
|
|
namespace SVSim.UnitTests.Services;
|
|
|
|
public class GameCalendarServiceTests
|
|
{
|
|
private sealed class FakeConfig : IGameConfigService
|
|
{
|
|
private readonly int _hour;
|
|
public FakeConfig(int hour) { _hour = hour; }
|
|
public T Get<T>() where T : class, new() =>
|
|
(T)(object)new GameCalendarConfig { DailyResetUtcHour = _hour };
|
|
}
|
|
|
|
private static GameCalendarService NewService(int resetHour) =>
|
|
new(new FakeConfig(resetHour));
|
|
|
|
private static DateTimeOffset Utc(int y, int mo, int d, int h, int m, int s) =>
|
|
new(y, mo, d, h, m, s, TimeSpan.Zero);
|
|
|
|
// ------------ ResetReady ------------
|
|
|
|
[Test]
|
|
public void ResetReady_returns_true_when_lastCheckpoint_is_null()
|
|
{
|
|
var svc = NewService(0);
|
|
Assert.That(svc.ResetReady(null), Is.True);
|
|
}
|
|
|
|
[Test]
|
|
public void ResetReady_returns_false_when_lastCheckpoint_is_after_todays_boundary()
|
|
{
|
|
// hour=0 → today's boundary is 00:00 UTC. lastCheckpoint at 00:00:01 UTC today
|
|
// is AFTER the boundary → not yet time to reset.
|
|
var svc = NewService(0);
|
|
var last = DateTime.UtcNow.Date.AddSeconds(1);
|
|
Assert.That(svc.ResetReady(last), Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public void ResetReady_returns_true_when_lastCheckpoint_is_before_todays_boundary()
|
|
{
|
|
var svc = NewService(0);
|
|
var last = DateTime.UtcNow.Date.AddSeconds(-1); // yesterday, 1s before midnight
|
|
Assert.That(svc.ResetReady(last), Is.True);
|
|
}
|
|
|
|
[Test]
|
|
public void ResetReady_respects_config_reset_hour()
|
|
{
|
|
// hour=17. lastCheckpoint at 16:00 UTC today = before today's boundary → reset ready.
|
|
// (unless we're currently before 17:00 UTC — in which case today's boundary is
|
|
// "tomorrow-in-the-future" and MostRecent is yesterday 17:00; last at 16:00 today
|
|
// is AFTER yesterday's 17:00 → NOT ready. Test both sides explicitly rather than
|
|
// relying on wall clock.)
|
|
var svc = NewService(17);
|
|
var now = DateTime.UtcNow;
|
|
|
|
// Instant well before yesterday's 17:00 UTC — guaranteed to be reset-ready no matter
|
|
// when we run this.
|
|
var stale = now.AddDays(-2);
|
|
Assert.That(svc.ResetReady(stale), Is.True, "48h ago is always reset-ready");
|
|
}
|
|
|
|
// ------------ DayKey ------------
|
|
|
|
[Test]
|
|
public void DayKey_hour0_uses_utc_calendar_day()
|
|
{
|
|
var svc = NewService(0);
|
|
Assert.That(svc.DayKey(Utc(2026, 6, 1, 0, 0, 0)), Is.EqualTo("day:2026-06-01"));
|
|
Assert.That(svc.DayKey(Utc(2026, 6, 1, 23, 59, 59)), Is.EqualTo("day:2026-06-01"));
|
|
Assert.That(svc.DayKey(Utc(2026, 6, 2, 0, 0, 0)), Is.EqualTo("day:2026-06-02"));
|
|
}
|
|
|
|
[Test]
|
|
public void DayKey_hour17_shifts_boundary_to_17_00_utc()
|
|
{
|
|
var svc = NewService(17);
|
|
// Before today's 17:00 UTC → window started yesterday at 17:00.
|
|
Assert.That(svc.DayKey(Utc(2026, 6, 1, 16, 59, 59)), Is.EqualTo("day:2026-05-31"));
|
|
// 17:00 UTC exactly → new window starts.
|
|
Assert.That(svc.DayKey(Utc(2026, 6, 1, 17, 0, 0)), Is.EqualTo("day:2026-06-01"));
|
|
// Later same day → same window.
|
|
Assert.That(svc.DayKey(Utc(2026, 6, 1, 23, 59, 59)), Is.EqualTo("day:2026-06-01"));
|
|
}
|
|
|
|
// ------------ WeekKey / MonthKey ------------
|
|
|
|
[Test]
|
|
public void WeekKey_is_iso_week_of_window_start()
|
|
{
|
|
var svc = NewService(0);
|
|
// 2026-05-25 is Monday (ISO week 22 of 2026).
|
|
Assert.That(svc.WeekKey(Utc(2026, 5, 25, 12, 0, 0)), Is.EqualTo("week:2026-W22"));
|
|
// Sunday of that week.
|
|
Assert.That(svc.WeekKey(Utc(2026, 5, 24, 23, 59, 59)), Is.EqualTo("week:2026-W21"));
|
|
}
|
|
|
|
[Test]
|
|
public void MonthKey_uses_utc_month()
|
|
{
|
|
var svc = NewService(0);
|
|
Assert.That(svc.MonthKey(Utc(2026, 6, 30, 23, 59, 59)), Is.EqualTo("month:2026-06"));
|
|
Assert.That(svc.MonthKey(Utc(2026, 7, 1, 0, 0, 0)), Is.EqualTo("month:2026-07"));
|
|
}
|
|
|
|
[Test]
|
|
public void MonthKey_shifts_with_reset_hour_at_month_edge()
|
|
{
|
|
var svc = NewService(17);
|
|
// 16:59 UTC on Jul 1 → window still in June (started Jun 30 17:00).
|
|
Assert.That(svc.MonthKey(Utc(2026, 7, 1, 16, 59, 0)), Is.EqualTo("month:2026-06"));
|
|
Assert.That(svc.MonthKey(Utc(2026, 7, 1, 17, 0, 0)), Is.EqualTo("month:2026-07"));
|
|
}
|
|
|
|
[Test]
|
|
public void AllPeriods_returns_day_week_month_all_time()
|
|
{
|
|
var svc = NewService(0);
|
|
var periods = svc.AllPeriods(Utc(2026, 5, 27, 12, 0, 0));
|
|
Assert.That(periods, Is.EquivalentTo(new[] {
|
|
"day:2026-05-27", "week:2026-W22", "month:2026-05", GameCalendarService.AllTime,
|
|
}));
|
|
}
|
|
}
|