using Domain.Entities.Stocks; using Domain.Entities.Stocks.ValueObject; using Infrastructure.StockData; using Microsoft.EntityFrameworkCore; namespace Application.Tests; /// /// SeibroIssuerSyncService 의 DB 반영 로직(SeibroIssuerImport) 통합 검증 — HTTP 는 파서 입력을 fixture 로 대체 /// (KRX 테스트들이 파서/로직을 분리 검증하는 방식 미러링). 실제 SQL Server(LocalDB)로 UQ(Market, ShortCode)·스탬핑 동작 확인. /// [TestClass] public sealed class SeibroIssuerSyncTests { [TestMethod] public async Task UpsertIssuerCodes_FromSampleXml_InsertsAll_ThenIdempotent() { // 기대 행위: fixture 전 행 insert → 동일 스냅샷 재실행은 무변경 (UQ Market+ShortCode upsert, resumable) var rows = SeibroShotnByMartParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getShotnByMart.xml"))); using (var db = TestDb.Create()) { var (inserted, updated) = await SeibroIssuerImport.UpsertIssuerCodesAsync(db, "13", rows, CancellationToken.None); Assert.AreEqual(rows.Count, inserted, "첫 실행은 전 행 insert"); Assert.AreEqual(0, updated); } using (var db = TestDb.Create()) { var (inserted, updated) = await SeibroIssuerImport.UpsertIssuerCodesAsync(db, "13", rows, CancellationToken.None); Assert.AreEqual(0, inserted, "동일 스냅샷 재실행은 insert 없음"); Assert.AreEqual(0, updated, "무변경 행은 update 로 세지 않는다 (UpdatedAt 미갱신)"); var saved = await db.SeibroIssuerCode.AsNoTracking().Where(c => c.Market == "13" && c.ShortCode == "000800").SingleAsync(); Assert.AreEqual("경남기업", saved.Name); Assert.AreEqual(80, saved.IssucoCustno); } } [TestMethod] public async Task UpsertIssuerCodes_ChangedNameOrCustno_UpdatesRow() { var original = new List { new("777001", "변경전상호", 91001) }; var renamed = new List { new("777001", "변경후상호", 91002) }; using (var db = TestDb.Create()) { await SeibroIssuerImport.UpsertIssuerCodesAsync(db, "11", original, CancellationToken.None); } using (var db = TestDb.Create()) { var (inserted, updated) = await SeibroIssuerImport.UpsertIssuerCodesAsync(db, "11", renamed, CancellationToken.None); Assert.AreEqual(0, inserted); Assert.AreEqual(1, updated, "이름/회사번호 변경 행은 update"); var saved = await db.SeibroIssuerCode.AsNoTracking().Where(c => c.Market == "11" && c.ShortCode == "777001").SingleAsync(); Assert.AreEqual("변경후상호", saved.Name); Assert.AreEqual(91002, saved.IssucoCustno); } } [TestMethod] public async Task UpsertIssuerCodes_SameShortCode_DifferentMarket_BothPersisted() { // 기대 행위: UQ 는 (Market, ShortCode) — 같은 단축코드라도 시장이 다르면 별도 행 (K-OTC 13 vs 코넥스 14) var rows = new List { new("777002", "양시장종목", 91003) }; using var db = TestDb.Create(); await SeibroIssuerImport.UpsertIssuerCodesAsync(db, "13", rows, CancellationToken.None); await SeibroIssuerImport.UpsertIssuerCodesAsync(db, "14", rows, CancellationToken.None); var saved = await db.SeibroIssuerCode.AsNoTracking().Where(c => c.ShortCode == "777002").ToListAsync(); Assert.AreEqual(2, saved.Count); } [TestMethod] public async Task StampStockIssucoCustno_MatchesByCode_SkipsUnchangedAndUnmapped() { // 기대 행위: Stock.Code = 단축코드 조인으로 스탬핑, 이미 같은 값이면 무변경, 매핑 없는 종목은 null 유지 using (var db = TestDb.Create()) { await db.Stock.AddAsync(Stock.Create("890001", "세이브로스탬핑1", StockMarket.KOSPI, new DateOnly(2020, 1, 2))); await db.Stock.AddAsync(Stock.Create("890002", "세이브로스탬핑2", StockMarket.KOSDAQ, new DateOnly(2020, 1, 2))); await db.SaveChangesAsync(default); } var custnoByShortCode = new Dictionary { ["890001"] = 77001 }; using (var db = TestDb.Create()) { var stamped = await SeibroIssuerImport.StampStockIssucoCustnoAsync(db, custnoByShortCode, CancellationToken.None); Assert.AreEqual(1, stamped, "매핑된 종목만 스탬핑"); } using (var db = TestDb.Create()) { var stampedAgain = await SeibroIssuerImport.StampStockIssucoCustnoAsync(db, custnoByShortCode, CancellationToken.None); Assert.AreEqual(0, stampedAgain, "이미 같은 값이면 무변경 (no-op 재실행)"); var s1 = await db.Stock.AsNoTracking().Where(c => c.Code == "890001").SingleAsync(); var s2 = await db.Stock.AsNoTracking().Where(c => c.Code == "890002").SingleAsync(); Assert.AreEqual(77001, s1.IssucoCustno); Assert.IsNull(s2.IssucoCustno, "스냅샷에 없는 종목은 null 유지"); } } [TestMethod] public async Task EnrichFromSeibro_StkStatFixture_PersistsExtensionColumns() { // 기대 행위: getStkStatInfo fixture 행으로 Stock 확장 5컬럼이 채워지고 DB 에 보존된다 (rolling 보강 경로) var row = SeibroStkStatParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getStkStatInfo.xml")))[0]; using (var db = TestDb.Create()) { var stock = Stock.Create("890003", "세이브로보강", StockMarket.KOSPI, new DateOnly(2020, 1, 2)); stock.EnrichFromSeibro(row.IssucoCustno, row.FaceValue, row.VotingRights, row.TotalIssuedShares, row.IsElectronicSecurity); await db.Stock.AddAsync(stock); await db.SaveChangesAsync(default); } using (var db = TestDb.Create()) { var saved = await db.Stock.AsNoTracking().Where(c => c.Code == "890003").SingleAsync(); Assert.AreEqual(593, saved.IssucoCustno); Assert.AreEqual(100m, saved.FaceValue); Assert.AreEqual(1m, saved.VotingRights); Assert.AreEqual(6419324700L, saved.TotalIssuedShares); Assert.IsFalse(saved.IsElectronicSecurity!.Value); } } }