using Domain.Entities.Stocks.ValueObject; using Infrastructure.StockData; namespace Application.Tests; [TestClass] public sealed class KrxStockParserTests { [TestMethod] public void ParseDailyPrices_MapsFields_FromSampleJson() { // KRX OpenAPI(stk_bydd_trd) 표준 응답 샘플 — ISU_CD=6자리 단축코드, FLUC_RT 는 소수 % 그대로 저장(Bp 아님) const string json = """ {"OutBlock_1":[ {"BAS_DD":"20250102","ISU_CD":"005930","ISU_NM":"삼성전자","MKT_NM":"KOSPI","SECT_TP_NM":"","TDD_CLSPRC":"70,500","CMPPREVDD_PRC":"-500","FLUC_RT":"-0.70","TDD_OPNPRC":"71,000","TDD_HGPRC":"71,200","TDD_LWPRC":"70,300","ACC_TRDVOL":"12,345,678","ACC_TRDVAL":"870,000,000,000","MKTCAP":"420,870,169,275,000","LIST_SHRS":"5,969,782,550"}, {"BAS_DD":"20250102","ISU_CD":"000660","ISU_NM":"SK하이닉스","MKT_NM":"KOSPI","SECT_TP_NM":"","TDD_CLSPRC":"210000","CMPPREVDD_PRC":"3000","FLUC_RT":"1.45","TDD_OPNPRC":"208000","TDD_HGPRC":"211500","TDD_LWPRC":"207000","ACC_TRDVOL":"2345678","ACC_TRDVAL":"490000000000","MKTCAP":"-","LIST_SHRS":"-"} ]} """; var rows = KrxStockParser.ParseDailyPrices(json, StockMarket.KOSPI); Assert.AreEqual(2, rows.Count); var samsung = rows[0]; Assert.AreEqual("005930", samsung.Code); Assert.AreEqual(new DateOnly(2025, 1, 2), samsung.TradingDate); Assert.AreEqual(71000, samsung.Open, "콤마 천단위를 제거하고 시가 파싱"); Assert.AreEqual(71200, samsung.High); Assert.AreEqual(70300, samsung.Low); Assert.AreEqual(70500, samsung.Close); Assert.AreEqual(12345678L, samsung.Volume, "거래량 콤마 제거"); Assert.AreEqual(870000000000L, samsung.TradingValue, "거래대금 콤마 제거"); Assert.AreEqual(-500, samsung.ChangeAmount); Assert.AreEqual(-0.70m, samsung.ChangeRate, "FLUC_RT 는 소수 % 그대로 (DataGoKr fltRt 와 동일 스케일, Bp 아님)"); Assert.AreEqual(420870169275000L, samsung.MarketCap); Assert.AreEqual(5969782550L, samsung.ListedShares); var hynix = rows[1]; Assert.AreEqual("000660", hynix.Code); Assert.AreEqual(1.45m, hynix.ChangeRate); Assert.IsNull(hynix.MarketCap, "\"-\" 시가총액은 null"); Assert.IsNull(hynix.ListedShares, "\"-\" 상장주식수는 null"); } [TestMethod] public void ParseDailyPrices_NormalizesPrefixedCode() { // 'A' 프리픽스 단축코드는 뒤 6자리로 정규화 const string json = """ {"OutBlock_1":[ {"BAS_DD":"20250102","ISU_CD":"A000660","ISU_NM":"SK하이닉스","MKT_NM":"KOSPI","TDD_CLSPRC":"210000","CMPPREVDD_PRC":"0","FLUC_RT":"0.00","TDD_OPNPRC":"210000","TDD_HGPRC":"210000","TDD_LWPRC":"210000","ACC_TRDVOL":"0","ACC_TRDVAL":"0","MKTCAP":"0","LIST_SHRS":"0"} ]} """; var rows = KrxStockParser.ParseDailyPrices(json, StockMarket.KOSPI); Assert.AreEqual(1, rows.Count); Assert.AreEqual("000660", rows[0].Code, "'A' 프리픽스 단축코드는 뒤 6자리로 정규화한다"); } [TestMethod] public void ParseDailyPrices_EmptyOrMissingBlock_ReturnsEmpty() { // basDd 미반영 — OutBlock_1 이 빈 배열 Assert.AreEqual(0, KrxStockParser.ParseDailyPrices("""{"OutBlock_1":[]}""", StockMarket.KOSDAQ).Count); // 인증 실패 등 OutBlock_1 노드 자체가 없는 응답도 안전하게 0건 Assert.AreEqual(0, KrxStockParser.ParseDailyPrices("""{"respMsg":"Unauthorized API Call","respCode":"401"}""", StockMarket.KOSPI).Count); } [TestMethod] public void ParseMasterInfo_MapsFields_KospiAndKosdaq() { // KRX OpenAPI(stk_isu_base_info/ksq_isu_base_info) 표준 응답 샘플 — ISU_SRT_CD=단축코드, ISU_CD=표준코드 ISIN const string kospiJson = """ {"OutBlock_1":[ {"ISU_CD":"KR7005930003","ISU_SRT_CD":"005930","ISU_NM":"삼성전자보통주","ISU_ABBRV":"삼성전자","ISU_ENG_NM":"SAMSUNG ELECTRONICS CO.,LTD.","LIST_DD":"19750611","MKT_TP_NM":"KOSPI","SECUGRP_NM":"주권","SECT_TP_NM":"","KIND_STKCERT_TP_NM":"보통주","PARVAL":"100","LIST_SHRS":"5969782550"} ]} """; var kospi = KrxStockParser.ParseMasterInfo(kospiJson, StockMarket.KOSPI); Assert.AreEqual(1, kospi.Count); var samsung = kospi[0]; Assert.AreEqual("005930", samsung.Code, "ISU_SRT_CD → Code"); Assert.AreEqual("KR7005930003", samsung.Isin, "ISU_CD(표준코드) → ISIN"); Assert.AreEqual("삼성전자", samsung.Name, "ISU_ABBRV(약칭) → Name"); Assert.AreEqual("SAMSUNG ELECTRONICS CO.,LTD.", samsung.EnglishName); Assert.AreEqual(StockMarket.KOSPI, samsung.Market, "market 은 호출 엔드포인트 인자로 결정"); Assert.IsNull(samsung.SectorName, "빈 SECT_TP_NM 은 null"); Assert.AreEqual(new DateOnly(1975, 6, 11), samsung.ListedDate, "LIST_DD → ListedDate"); // KOSDAQ 은 SECT_TP_NM(소속부) 이 채워져 온다 const string kosdaqJson = """ {"OutBlock_1":[ {"ISU_CD":"KR7058820002","ISU_SRT_CD":"058820","ISU_NM":"CMG제약","ISU_ABBRV":"CMG제약","ISU_ENG_NM":"CMG Pharmaceutical Co., Ltd.","LIST_DD":"20010831","MKT_TP_NM":"KOSDAQ","SECUGRP_NM":"주권","SECT_TP_NM":"우량기업부","KIND_STKCERT_TP_NM":"보통주","PARVAL":"500","LIST_SHRS":"111111111"} ]} """; var kosdaq = KrxStockParser.ParseMasterInfo(kosdaqJson, StockMarket.KOSDAQ); Assert.AreEqual(1, kosdaq.Count); Assert.AreEqual(StockMarket.KOSDAQ, kosdaq[0].Market); Assert.AreEqual("우량기업부", kosdaq[0].SectorName, "KOSDAQ SECT_TP_NM(소속부) → SectorName"); } [TestMethod] public void ParseMasterInfo_InvalidIsin_And_EmptyBlock() { // ISU_CD 가 12자리가 아니면 ISIN=null (방어) const string json = """ {"OutBlock_1":[ {"ISU_CD":"BADISIN","ISU_SRT_CD":"005930","ISU_NM":"삼성전자보통주","ISU_ABBRV":"삼성전자","ISU_ENG_NM":"","LIST_DD":"19750611","MKT_TP_NM":"KOSPI","SECT_TP_NM":"","LIST_SHRS":"1"} ]} """; var rows = KrxStockParser.ParseMasterInfo(json, StockMarket.KOSPI); Assert.AreEqual(1, rows.Count); Assert.IsNull(rows[0].Isin, "12자리 아닌 표준코드는 ISIN=null"); Assert.IsNull(rows[0].EnglishName, "빈 ISU_ENG_NM 은 null"); // 빈 스냅샷 / 노드 없음 안전 처리 Assert.AreEqual(0, KrxStockParser.ParseMasterInfo("""{"OutBlock_1":[]}""", StockMarket.KOSPI).Count); Assert.AreEqual(0, KrxStockParser.ParseMasterInfo("""{"respCode":"401"}""", StockMarket.KOSDAQ).Count); } }