using Application.Helpers; using Infrastructure.StockData; using Microsoft.EntityFrameworkCore; namespace Application.Tests; /// /// SeibroSupplySyncService 의 DB 반영 로직(SeibroSupplyImport) 통합 검증 — HTTP 는 파서 입력을 fixture 로 대체 /// (SeibroDividendSyncTests 방식 미러링). 실제 SQL Server(LocalDB)로 대차 upsert·보호예수/유통변경 window·증감 rolling·비상장 확인. /// [TestClass] public sealed class SeibroSupplySyncTests { [TestMethod] public async Task UpsertLending_FromSampleXml_StampsIsinAndDate_ThenIdempotent() { // 기대 행위: getSlbDealingByIsin(1행) — 응답에 없는 Isin/StdDt 를 요청값으로 스탬핑 upsert (UQ Isin+StdDt). 재실행 무변경. var rows = SeibroSlbDealingParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getSlbDealingByIsin.xml"))); var isin = $"KR7{Guid.NewGuid().ToString("N")[..9]}"; var stdDt = new DateOnly(2018, 8, 22); using (var db = TestDb.Create()) { var (inserted, updated) = await SeibroSupplyImport.UpsertLendingAsync(db, isin, stdDt, rows, CancellationToken.None); Assert.AreEqual(1, inserted); Assert.AreEqual(0, updated); } using (var db = TestDb.Create()) { var (inserted, updated) = await SeibroSupplyImport.UpsertLendingAsync(db, isin, stdDt, rows, CancellationToken.None); Assert.AreEqual(0, inserted, "재실행 idempotent"); Assert.AreEqual(0, updated, "무변경 no-op"); var saved = await db.SecuritiesLending.AsNoTracking().Where(c => c.Isin == isin && c.StdDt == stdDt).SingleAsync(); Assert.AreEqual(21976319L, saved.SlbTrRemQty); Assert.AreEqual(0.01m, saved.FrinerLendRemRatio); } } [TestMethod] public async Task ReplaceLockupWindow_FromSampleXml_DeletesThenInserts() { // 기대 행위: getSafeDpDutyDepoStatus — 예수일 window(day) delete+insert. 같은 날 재실행은 delete=1,insert=1 로 재구성. var rows = SeibroLockupParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getSafeDpDutyDepoStatus.xml"))); var day = new DateOnly(2019, 2, 25); using (var db = TestDb.Create()) { var (inserted, deleted) = await SeibroSupplyImport.ReplaceLockupWindowAsync(db, day, day, rows, CancellationToken.None); Assert.AreEqual(1, inserted); Assert.AreEqual(0, deleted, "첫 창은 삭제할 기존 행 없음"); } using (var db = TestDb.Create()) { var (inserted, deleted) = await SeibroSupplyImport.ReplaceLockupWindowAsync(db, day, day, rows, CancellationToken.None); Assert.AreEqual(1, inserted, "window 재구성 → insert"); Assert.AreEqual(1, deleted, "기존 창 삭제"); var saved = await db.LockupEvent.AsNoTracking().Where(c => c.SafedpDt == day).ToListAsync(); Assert.AreEqual(1, saved.Count, "중복 없이 1행"); Assert.AreEqual("케이비아이동국실업", saved[0].KorSecnNm); Assert.AreEqual("02", saved[0].DutySafedpRacd); } } [TestMethod] public async Task UpsertIssuanceChanges_FromSampleXml_KeepsBothSameNtimesRows() { // 기대 행위: getStkIncdecDetails(3행) upsert. 동일 발행횟수(75) 2행이 IssuDt/Racd 로 구분되어 둘 다 저장. var rows = SeibroStkIncdecParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getStkIncdecDetails.xml"))); using (var db = TestDb.Create()) { var (inserted, _) = await SeibroSupplyImport.UpsertIssuanceChangesAsync(db, rows, CancellationToken.None); Assert.AreEqual(3, inserted); } using (var db = TestDb.Create()) { var (inserted, updated) = await SeibroSupplyImport.UpsertIssuanceChangesAsync(db, rows, CancellationToken.None); Assert.AreEqual(0, inserted, "재실행 idempotent"); Assert.AreEqual(0, updated); var ntimes75 = await db.StockIssuanceChange.AsNoTracking().Where(c => c.Isin == "KR7005930003" && c.SecnIssuNtimes == 75).ToListAsync(); Assert.AreEqual(2, ntimes75.Count, "발행횟수 75 2행 보존 (충돌 없음)"); } } [TestMethod] public async Task ReplaceListingChangeWindow_FromSampleXml_ReconstructsWindow() { // 기대 행위: getStkListInfo — 적용일 window(2018-12) delete+insert. 재실행은 창 재구성(insert=deleted). var rows = SeibroStkListParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getStkListInfo.xml"))); var begin = new DateOnly(2018, 12, 1); var end = new DateOnly(2018, 12, 31); int firstInserted; using (var db = TestDb.Create()) { var (inserted, deleted) = await SeibroSupplyImport.ReplaceListingChangeWindowAsync(db, begin, end, rows, CancellationToken.None); Assert.IsTrue(inserted > 0, "12월 유통변경 다수 insert"); Assert.AreEqual(0, deleted); firstInserted = inserted; } using (var db = TestDb.Create()) { var (inserted, deleted) = await SeibroSupplyImport.ReplaceListingChangeWindowAsync(db, begin, end, rows, CancellationToken.None); Assert.AreEqual(firstInserted, inserted, "동일 창 재구성"); Assert.AreEqual(firstInserted, deleted, "기존 창 전부 삭제 후 재삽입"); var etf = await db.ListingChangeEvent.AsNoTracking().Where(c => c.Isin == "KR7314700006").SingleAsync(); Assert.AreEqual("110", etf.AfaltCirclForm); } } [TestMethod] public async Task UpsertUnlisted_FromSampleXml_StampsStdDt() { // 기대 행위: getUnlistCirclInfo(1행) — StdDt 요청값 스탬핑 upsert (UQ Isin+StdDt). var rows = SeibroUnlistCirclParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getUnlistCirclInfo.xml"))); var stdDt = new DateOnly(2018, 8, 7); using (var db = TestDb.Create()) { var (inserted, _) = await SeibroSupplyImport.UpsertUnlistedAsync(db, stdDt, rows, CancellationToken.None); Assert.AreEqual(1, inserted); } using (var db = TestDb.Create()) { var (inserted, updated) = await SeibroSupplyImport.UpsertUnlistedAsync(db, stdDt, rows, CancellationToken.None); Assert.AreEqual(0, inserted, "재실행 idempotent"); Assert.AreEqual(0, updated); var saved = await db.UnlistedCirculation.AsNoTracking().Where(c => c.Isin == "KR7064400005" && c.StdDt == stdDt).SingleAsync(); Assert.AreEqual(1400L, saved.AcnttrQty); Assert.AreEqual("보통주", saved.SecnKacdNm); } } [TestMethod] public void Quota_StockBudget_StopsAtCap() { // 기대 행위: 주식 카테고리 예산 상한 준수 — 상한까지만 소비 성공, 초과분은 false + Remaining 0 var quota = new SeibroQuota(() => new DateOnly(2026, 7, 7)); const int budget = 5; var consumed = 0; for (var i = 0; i < 10; i++) { if (quota.TryConsume(SeibroCategory.Stock, 1, budget)) { consumed++; } } Assert.AreEqual(budget, consumed, "예산(5) 만큼만 소비"); Assert.AreEqual(0, quota.Remaining(SeibroCategory.Stock, budget)); } }