using Infrastructure.StockData; namespace Application.Tests; [TestClass] public sealed class WorldIndexParserTests { [TestMethod] public void Parse_MapsFields_LowercasesSymbol() { // Stooq 라이트 배치(/q/l/?f=sd2t2ohlc&h&e=csv) 표준 응답 — 헤더 + 2행 const string csv = """ Symbol,Date,Time,Open,High,Low,Close ^SPX,2026-07-07,22:15:00,5100.50,5130.00,5090.10,5125.30 ^NKX,2026-07-07,06:00:00,39000,39250,38900,39180.5 """; var rows = WorldIndexCsvParser.Parse(csv); Assert.AreEqual(2, rows.Count); var spx = rows[0]; Assert.AreEqual("^spx", spx.Symbol, "심볼은 소문자 정규화"); Assert.AreEqual(new DateOnly(2026, 7, 7), spx.TradeDate); Assert.AreEqual(5100.50m, spx.Open); Assert.AreEqual(5130.00m, spx.High); Assert.AreEqual(5090.10m, spx.Low); Assert.AreEqual(5125.30m, spx.Close); var nkx = rows[1]; Assert.AreEqual("^nkx", nkx.Symbol); Assert.AreEqual(39180.5m, nkx.Close); } [TestMethod] public void Parse_MissingClose_SkipsRow_MissingOhlc_FallsBackToClose() { // 첫 행: Close 가 N/D → 버림. 둘째 행: OHLC 가 N/D 지만 Close 유효 → OHLC 는 Close 로 보정. const string csv = """ Symbol,Date,Time,Open,High,Low,Close ^BAD,N/D,N/D,N/D,N/D,N/D,N/D ^HSI,2026-07-07,10:00:00,N/D,N/D,N/D,24500.75 """; var rows = WorldIndexCsvParser.Parse(csv); Assert.AreEqual(1, rows.Count, "Close 결측(N/D) 행은 제외"); var hsi = rows[0]; Assert.AreEqual("^hsi", hsi.Symbol); Assert.AreEqual(24500.75m, hsi.Close); Assert.AreEqual(24500.75m, hsi.Open, "OHLC 결측은 Close 로 보정"); Assert.AreEqual(24500.75m, hsi.High); Assert.AreEqual(24500.75m, hsi.Low); } [TestMethod] public void Parse_EmptyOrHeaderOnly_ReturnsEmpty() { Assert.AreEqual(0, WorldIndexCsvParser.Parse("").Count); Assert.AreEqual(0, WorldIndexCsvParser.Parse(" ").Count); Assert.AreEqual(0, WorldIndexCsvParser.Parse("Symbol,Date,Time,Open,High,Low,Close").Count, "헤더만 있으면 0건"); } }