"""price_fetcher._select_price_from_response 단위 테스트. 실행: cd web-backend/stock-lab python -m unittest app.test_price_fetcher -v """ import os import sys import unittest # app 패키지를 직접 실행 가능하도록 sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from app.price_fetcher import _select_price_from_response class SelectPriceFromResponseTest(unittest.TestCase): def test_regular_session_uses_close_price(self): """정규장 운영 중이면 closePrice를 REGULAR 세션으로 반환.""" payload = { "closePrice": "70,500", "marketStatus": "OPEN", "localTradedAt": "2026-05-11T11:23:45+09:00", "overMarketPriceInfo": None, } result = _select_price_from_response(payload) self.assertEqual(result["price"], 70500) self.assertEqual(result["session"], "REGULAR") self.assertEqual(result["as_of"], "2026-05-11T11:23:45+09:00") def test_nxt_after_market_open_uses_over_price(self): """정규장 마감 + NXT 애프터마켓 운영중이면 overPrice를 NXT_AFTER 세션으로 반환.""" payload = { "closePrice": "285,500", "marketStatus": "CLOSE", "localTradedAt": "2026-05-11T15:30:00+09:00", "overMarketPriceInfo": { "tradingSessionType": "AFTER_MARKET", "overMarketStatus": "OPEN", "overPrice": "285,000", "localTradedAt": "2026-05-11T19:21:40+09:00", "tradeStopType": {"name": "TRADING"}, }, } result = _select_price_from_response(payload) self.assertEqual(result["price"], 285000) self.assertEqual(result["session"], "NXT_AFTER") self.assertEqual(result["as_of"], "2026-05-11T19:21:40+09:00") def test_nxt_pre_market_open_uses_over_price(self): """NXT 프리마켓 운영중이면 NXT_PRE 세션 + overPrice.""" payload = { "closePrice": "70,500", "marketStatus": "CLOSE", "localTradedAt": "2026-05-10T15:30:00+09:00", "overMarketPriceInfo": { "tradingSessionType": "PRE_MARKET", "overMarketStatus": "OPEN", "overPrice": "70,800", "localTradedAt": "2026-05-11T08:30:00+09:00", "tradeStopType": {"name": "TRADING"}, }, } result = _select_price_from_response(payload) self.assertEqual(result["price"], 70800) self.assertEqual(result["session"], "NXT_PRE") self.assertEqual(result["as_of"], "2026-05-11T08:30:00+09:00") def test_nxt_closed_falls_back_to_close_price(self): """NXT가 CLOSE 상태이면 closePrice 사용, 세션은 CLOSED.""" payload = { "closePrice": "285,500", "marketStatus": "CLOSE", "localTradedAt": "2026-05-11T15:30:00+09:00", "overMarketPriceInfo": { "tradingSessionType": "AFTER_MARKET", "overMarketStatus": "CLOSE", "overPrice": "285,000", "tradeStopType": {"name": "TRADING"}, }, } result = _select_price_from_response(payload) self.assertEqual(result["price"], 285500) self.assertEqual(result["session"], "CLOSED") def test_nxt_trading_halted_falls_back_to_close_price(self): """NXT OPEN이지만 tradeStopType이 TRADING이 아니면 closePrice 사용.""" payload = { "closePrice": "285,500", "marketStatus": "CLOSE", "overMarketPriceInfo": { "tradingSessionType": "AFTER_MARKET", "overMarketStatus": "OPEN", "overPrice": "285,000", "tradeStopType": {"name": "STOP"}, }, } result = _select_price_from_response(payload) self.assertEqual(result["price"], 285500) self.assertEqual(result["session"], "CLOSED") def test_no_over_market_info_returns_close_price(self): """overMarketPriceInfo 자체가 없는 경우(해외 종목 등) closePrice 그대로.""" payload = { "closePrice": "150,000", "marketStatus": "CLOSE", "localTradedAt": "2026-05-11T15:30:00+09:00", } result = _select_price_from_response(payload) self.assertEqual(result["price"], 150000) self.assertEqual(result["session"], "CLOSED") def test_missing_close_price_returns_none(self): """closePrice가 없거나 비숫자면 price는 None.""" payload = {"closePrice": "", "marketStatus": "CLOSE"} result = _select_price_from_response(payload) self.assertIsNone(result["price"]) def test_alternate_stock_end_price_field(self): """일부 응답은 stockEndPrice 필드를 사용 — 폴백 인식.""" payload = { "stockEndPrice": "12,345", "marketStatus": "OPEN", } result = _select_price_from_response(payload) self.assertEqual(result["price"], 12345) self.assertEqual(result["session"], "REGULAR") if __name__ == "__main__": unittest.main()