using Infrastructure.StockData; namespace Application.Tests; [TestClass] public sealed class MarketQuoteParserTests { [TestMethod] public void Parse_WithVolume_MapsFields_LowercasesSymbol() { // Stooq 라이트 배치(/q/l/?f=sd2t2ohlcv&h&e=csv) — 헤더 + 2행 (거래량 포함) const string csv = """ Symbol,Date,Time,Open,High,Low,Close,Volume AAPL.US,2026-07-07,22:00:00,210.50,213.00,209.10,212.30,54812000 MSFT.US,2026-07-07,22:00:00,430,435.5,428,433.2,21500000 """; var rows = MarketQuoteCsvParser.Parse(csv); Assert.AreEqual(2, rows.Count); var aapl = rows[0]; Assert.AreEqual("aapl.us", aapl.Symbol, "심볼은 소문자 정규화"); Assert.AreEqual(new DateOnly(2026, 7, 7), aapl.TradeDate); Assert.AreEqual(210.50m, aapl.Open); Assert.AreEqual(212.30m, aapl.Close); Assert.AreEqual(54812000L, aapl.Volume); Assert.AreEqual(433.2m, rows[1].Close); Assert.AreEqual(21500000L, rows[1].Volume); } [TestMethod] public void Parse_MissingVolume_DefaultsZero_MissingClose_SkipsRow() { // 첫 행: Close 가 N/D → 버림. 둘째 행: 7컬럼(볼륨 미제공) + OHLC N/D → OHLC 는 Close 보정, Volume 0. const string csv = """ Symbol,Date,Time,Open,High,Low,Close ^BAD,N/D,N/D,N/D,N/D,N/D,N/D GOLD.F,2026-07-07,20:00:00,N/D,N/D,N/D,2410.5 """; var rows = MarketQuoteCsvParser.Parse(csv); Assert.AreEqual(1, rows.Count, "Close 결측(N/D) 행은 제외"); var gold = rows[0]; Assert.AreEqual("gold.f", gold.Symbol); Assert.AreEqual(2410.5m, gold.Close); Assert.AreEqual(2410.5m, gold.Open, "OHLC 결측은 Close 로 보정"); Assert.AreEqual(0L, gold.Volume, "볼륨 미제공은 0"); } [TestMethod] public void Parse_EmptyOrHeaderOnly_ReturnsEmpty() { Assert.AreEqual(0, MarketQuoteCsvParser.Parse("").Count); Assert.AreEqual(0, MarketQuoteCsvParser.Parse(" ").Count); Assert.AreEqual(0, MarketQuoteCsvParser.Parse("Symbol,Date,Time,Open,High,Low,Close,Volume").Count, "헤더만 있으면 0건"); } }