@@ -642,3 +642,126 @@ def test_to_dict_with_all_fields(self):
642642 assert result ["evaluation" ] == 11670000.0
643643 assert result ["analysis_id" ] == 123
644644 assert len (result ["holdings" ]) == 1
645+
646+
647+ class TestFetchMissingPricesOverseas :
648+ """해외주식 _fetch_missing_prices 테스트 - TOSS 전용 해외 종목 현재가 조회"""
649+
650+ @pytest .mark .asyncio
651+ async def test_fetch_overseas_price_for_toss_only_stock (
652+ self , merged_portfolio_service , mock_kis_client
653+ ):
654+ """TOSS만 보유한 해외 종목의 현재가 조회"""
655+ # 현재가가 0인 TOSS 전용 해외 종목
656+ merged = {
657+ "CONY" : MergedHolding (
658+ ticker = "CONY" ,
659+ name = "CONY" ,
660+ market_type = "US" ,
661+ current_price = 0.0 ,
662+ total_quantity = 20 ,
663+ toss_quantity = 20 ,
664+ toss_avg_price = 17.18 ,
665+ holdings = [HoldingInfo (broker = "toss" , quantity = 20 , avg_price = 17.18 )],
666+ )
667+ }
668+
669+ # KIS API 해외주식 현재가 응답 Mock
670+ price_df = pd .DataFrame ([{"close" : 18.50 }])
671+ mock_kis_client .inquire_overseas_price = AsyncMock (return_value = price_df )
672+
673+ await merged_portfolio_service ._fetch_missing_prices (
674+ merged , MarketType .US , mock_kis_client
675+ )
676+
677+ assert merged ["CONY" ].current_price == 18.50
678+ mock_kis_client .inquire_overseas_price .assert_called_once_with ("CONY" )
679+
680+ @pytest .mark .asyncio
681+ async def test_fetch_overseas_price_multiple_stocks (
682+ self , merged_portfolio_service , mock_kis_client
683+ ):
684+ """여러 TOSS 전용 해외 종목의 현재가 조회"""
685+ merged = {
686+ "CONY" : MergedHolding (
687+ ticker = "CONY" ,
688+ name = "CONY" ,
689+ market_type = "US" ,
690+ current_price = 0.0 ,
691+ total_quantity = 20 ,
692+ ),
693+ "BRK-B" : MergedHolding (
694+ ticker = "BRK-B" ,
695+ name = "버크셔 해서웨이 B" ,
696+ market_type = "US" ,
697+ current_price = 0.0 ,
698+ total_quantity = 5 ,
699+ ),
700+ }
701+
702+ # 각 종목별 현재가 응답
703+ def mock_inquire_overseas_price (ticker ):
704+ prices = {"CONY" : 18.50 , "BRK-B" : 474.17 }
705+ return pd .DataFrame ([{"close" : prices [ticker ]}])
706+
707+ mock_kis_client .inquire_overseas_price = AsyncMock (
708+ side_effect = mock_inquire_overseas_price
709+ )
710+
711+ await merged_portfolio_service ._fetch_missing_prices (
712+ merged , MarketType .US , mock_kis_client
713+ )
714+
715+ assert merged ["CONY" ].current_price == 18.50
716+ assert merged ["BRK-B" ].current_price == 474.17
717+ assert mock_kis_client .inquire_overseas_price .call_count == 2
718+
719+ @pytest .mark .asyncio
720+ async def test_skip_overseas_stocks_with_price (
721+ self , merged_portfolio_service , mock_kis_client
722+ ):
723+ """현재가가 이미 있는 해외 종목은 조회하지 않음"""
724+ merged = {
725+ "TSLA" : MergedHolding (
726+ ticker = "TSLA" ,
727+ name = "Tesla" ,
728+ market_type = "US" ,
729+ current_price = 250.0 , # 이미 현재가 있음
730+ total_quantity = 3 ,
731+ )
732+ }
733+
734+ mock_kis_client .inquire_overseas_price = AsyncMock ()
735+
736+ await merged_portfolio_service ._fetch_missing_prices (
737+ merged , MarketType .US , mock_kis_client
738+ )
739+
740+ mock_kis_client .inquire_overseas_price .assert_not_called ()
741+
742+ @pytest .mark .asyncio
743+ async def test_handle_overseas_api_error_gracefully (
744+ self , merged_portfolio_service , mock_kis_client
745+ ):
746+ """해외주식 API 오류 시 로그만 남기고 계속 진행"""
747+ merged = {
748+ "INVALID" : MergedHolding (
749+ ticker = "INVALID" ,
750+ name = "존재하지 않는 종목" ,
751+ market_type = "US" ,
752+ current_price = 0.0 ,
753+ total_quantity = 10 ,
754+ )
755+ }
756+
757+ mock_kis_client .inquire_overseas_price = AsyncMock (
758+ side_effect = Exception ("API Error: 해당종목정보가 없습니다" )
759+ )
760+
761+ # 예외 발생해도 에러 없이 진행
762+ await merged_portfolio_service ._fetch_missing_prices (
763+ merged , MarketType .US , mock_kis_client
764+ )
765+
766+ # 현재가는 0으로 유지
767+ assert merged ["INVALID" ].current_price == 0.0
0 commit comments