using Application.Helpers; namespace Application.Tests; [TestClass] public sealed class KrxBackfillTests { private static IReadOnlySet NoHolidays => new HashSet(); [TestMethod] public async Task RunAsync_IteratesBusinessDays_RecentFirst_SkippingWeekends() { // 2025-01-06(월) ~ 2025-01-10(금) 은 평일 5일, 2025-01-11(토)/12(일) 은 주말. // endDate=일(12), startDate=토(11 하루 전 없음) → 창을 넓게 잡고 주말이 제외되는지 확인. var start = new DateOnly(2025, 1, 6); // 월 var end = new DateOnly(2025, 1, 12); // 일 var fetchedDays = new List(); var count = await KrxBackfill.RunAsync( existsForDate: (_, _) => Task.FromResult(false), fetchAndUpsertForDate: (day, _) => { fetchedDays.Add(day); return Task.CompletedTask; }, startDate: start, endDate: end, holidays: NoHolidays, maxPerRun: 100, delayMs: 0, ct: CancellationToken.None); // 주말(11 토, 12 일) 제외 → 평일 5일 (10 금 → 6 월 순, recent-first) Assert.AreEqual(5, count); CollectionAssert.AreEqual( new[] { new DateOnly(2025, 1, 10), new DateOnly(2025, 1, 9), new DateOnly(2025, 1, 8), new DateOnly(2025, 1, 7), new DateOnly(2025, 1, 6) }, fetchedDays, "endDate 부터 과거로(recent-first), 주말은 건너뛴다"); } [TestMethod] public async Task RunAsync_SkipsHolidays() { var start = new DateOnly(2025, 1, 6); // 월 var end = new DateOnly(2025, 1, 10); // 금 var holidays = new HashSet { new(2025, 1, 8) }; // 수 휴장 var fetchedDays = new List(); var count = await KrxBackfill.RunAsync( existsForDate: (_, _) => Task.FromResult(false), fetchAndUpsertForDate: (day, _) => { fetchedDays.Add(day); return Task.CompletedTask; }, startDate: start, endDate: end, holidays: holidays, maxPerRun: 100, delayMs: 0, ct: CancellationToken.None); Assert.AreEqual(4, count, "휴장일(8 수) 제외 → 4일"); CollectionAssert.DoesNotContain(fetchedDays, new DateOnly(2025, 1, 8)); } [TestMethod] public async Task RunAsync_SkipsAlreadyExistingDays_Resumable() { var start = new DateOnly(2025, 1, 6); // 월 var end = new DateOnly(2025, 1, 10); // 금 // 8 수, 9 목 은 이미 적재됨 → skip var existing = new HashSet { new(2025, 1, 8), new(2025, 1, 9) }; var fetchedDays = new List(); var count = await KrxBackfill.RunAsync( existsForDate: (day, _) => Task.FromResult(existing.Contains(day)), fetchAndUpsertForDate: (day, _) => { fetchedDays.Add(day); return Task.CompletedTask; }, startDate: start, endDate: end, holidays: NoHolidays, maxPerRun: 100, delayMs: 0, ct: CancellationToken.None); Assert.AreEqual(3, count, "이미 적재된 2일 제외 → 3일 (resumable)"); CollectionAssert.AreEqual( new[] { new DateOnly(2025, 1, 10), new DateOnly(2025, 1, 7), new DateOnly(2025, 1, 6) }, fetchedDays); } [TestMethod] public async Task RunAsync_StopsAfterMaxPerRun() { // 넉넉한 창(3주) 에서 상한 3 으로 잘리는지 var start = new DateOnly(2025, 1, 1); var end = new DateOnly(2025, 1, 21); var fetchedDays = new List(); var count = await KrxBackfill.RunAsync( existsForDate: (_, _) => Task.FromResult(false), fetchAndUpsertForDate: (day, _) => { fetchedDays.Add(day); return Task.CompletedTask; }, startDate: start, endDate: end, holidays: NoHolidays, maxPerRun: 3, delayMs: 0, ct: CancellationToken.None); Assert.AreEqual(3, count, "maxPerRun 도달 시 즉시 멈춤"); Assert.AreEqual(3, fetchedDays.Count); // recent-first: 21(화),20(월),17(금) — 18/19 는 주말 CollectionAssert.AreEqual( new[] { new DateOnly(2025, 1, 21), new DateOnly(2025, 1, 20), new DateOnly(2025, 1, 17) }, fetchedDays); } [TestMethod] public async Task RunAsync_MaxPerRunZeroOrNegative_FetchesNothing() { var fetched = 0; var count = await KrxBackfill.RunAsync( existsForDate: (_, _) => Task.FromResult(false), fetchAndUpsertForDate: (_, _) => { fetched++; return Task.CompletedTask; }, startDate: new DateOnly(2025, 1, 1), endDate: new DateOnly(2025, 1, 31), holidays: NoHolidays, maxPerRun: 0, delayMs: 0, ct: CancellationToken.None); Assert.AreEqual(0, count); Assert.AreEqual(0, fetched, "maxPerRun<=0 이면 fetch 호출 자체가 없다"); } [TestMethod] public async Task RunAsync_EmptyWindow_WhenStartAfterEnd_FetchesNothing() { var fetched = 0; var count = await KrxBackfill.RunAsync( existsForDate: (_, _) => Task.FromResult(false), fetchAndUpsertForDate: (_, _) => { fetched++; return Task.CompletedTask; }, startDate: new DateOnly(2025, 2, 1), endDate: new DateOnly(2025, 1, 1), // start > end holidays: NoHolidays, maxPerRun: 100, delayMs: 0, ct: CancellationToken.None); Assert.AreEqual(0, count); Assert.AreEqual(0, fetched); } }