auto: 일일 백업 2026-09-30 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
import sys
|
||||
import unittest
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
SCRIPTS = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(SCRIPTS))
|
||||
|
||||
import ipo_calendar_sync as sync
|
||||
|
||||
|
||||
class IpoParserTests(unittest.TestCase):
|
||||
def test_naver_api_parser_includes_cutoff_date(self):
|
||||
payload = {
|
||||
'examinationList': [],
|
||||
'demandForecastingList': [],
|
||||
'forecastingCompleteList': [],
|
||||
'subscriptionList': [{
|
||||
'ipoCode': 'A0035S0',
|
||||
'compName': '빅웨이브로보틱스',
|
||||
'poStartDate': '2026-09-15',
|
||||
'poEndDate': '2026-09-16',
|
||||
'lcalDate': '2026-09-29',
|
||||
'orgNm': '한국투자증권',
|
||||
}],
|
||||
'subscriptionCompleteList': [],
|
||||
'listingList': [],
|
||||
}
|
||||
with patch.object(sync, 'fetch', return_value=sync.json.dumps(payload)):
|
||||
events = sync.parse_naver_api_events_from(date(2026, 9, 29))
|
||||
|
||||
self.assertEqual([(e.kind, e.name) for e in events], [
|
||||
('listing', '빅웨이브로보틱스'),
|
||||
])
|
||||
self.assertEqual(events[0].start_date, date(2026, 9, 29))
|
||||
|
||||
def test_38_parsers_read_subscription_and_listing_tables(self):
|
||||
subscription_html = '''
|
||||
<table summary="공모주 청약일정"><tbody>
|
||||
<tr><th>종목명</th><th>공모주일정</th><th>확정공모가</th><th>희망공모가</th><th>경쟁률</th><th>주간사</th></tr>
|
||||
<tr><td>넥스아이</td><td>2026.11.03~11.04</td><td>-</td><td>-</td><td></td><td>한국투자증권</td></tr>
|
||||
</tbody></table>'''
|
||||
listing_html = '''
|
||||
<table summary="신규상장종목"><tbody>
|
||||
<tr><th>기업명</th><th>신규상장일</th></tr>
|
||||
<tr><td>빅웨이브로보틱스</td><td>2026/09/29</td></tr>
|
||||
</tbody></table>'''
|
||||
|
||||
subscriptions = sync.parse_38_subscription_html(
|
||||
subscription_html, date(2026, 9, 29)
|
||||
)
|
||||
listings = sync.parse_38_listing_html(listing_html, date(2026, 9, 29))
|
||||
|
||||
self.assertEqual(subscriptions[0].start_date, date(2026, 11, 3))
|
||||
self.assertEqual(subscriptions[0].brokers, '한국투자증권')
|
||||
self.assertEqual(listings[0].name, '빅웨이브로보틱스')
|
||||
self.assertEqual(listings[0].start_date, date(2026, 9, 29))
|
||||
|
||||
def test_zero_primary_result_switches_to_38_fallback(self):
|
||||
fallback = sync.EventSpec(
|
||||
'listing', '빅웨이브로보틱스', date(2026, 9, 29),
|
||||
date(2026, 9, 29), '', sync.LISTING_URL,
|
||||
source_label='38커뮤니케이션 IPO',
|
||||
)
|
||||
with patch.object(sync, 'parse_naver_api_events_from', return_value=[]), patch.object(
|
||||
sync, 'parse_38_events_from', return_value=[fallback]
|
||||
):
|
||||
events, parser_used, errors = sync.collect_ipo_events(date(2026, 9, 29))
|
||||
|
||||
self.assertEqual(parser_used, '38_fallback')
|
||||
self.assertEqual(events, [fallback])
|
||||
self.assertEqual(errors, ['naver_api: 수집 결과 0건'])
|
||||
|
||||
def test_all_parser_failures_raise_structured_error(self):
|
||||
with patch.object(
|
||||
sync, 'parse_naver_api_events_from', side_effect=ValueError('schema changed')
|
||||
), patch.object(
|
||||
sync, 'parse_38_events_from', side_effect=ValueError('table changed')
|
||||
):
|
||||
with self.assertRaises(sync.IpoParserError) as caught:
|
||||
sync.collect_ipo_events(date(2026, 9, 29))
|
||||
|
||||
self.assertEqual(caught.exception.errors, [
|
||||
'naver_api: schema changed',
|
||||
'38_fallback: table changed',
|
||||
])
|
||||
|
||||
def test_parser_alert_is_deduplicated_and_recovery_is_reported(self):
|
||||
state = {}
|
||||
errors = ['naver_api: 수집 결과 0건']
|
||||
with patch.object(sync, 'send_telegram', return_value=True) as send:
|
||||
sync.update_parser_health(state, 'degraded', errors, 12)
|
||||
sync.update_parser_health(state, 'degraded', errors, 12)
|
||||
sync.update_parser_health(state, 'healthy', [], 18)
|
||||
|
||||
self.assertEqual(send.call_count, 2)
|
||||
self.assertIn('파서 일부 실패', send.call_args_list[0].args[0])
|
||||
self.assertIn('파서 복구', send.call_args_list[1].args[0])
|
||||
self.assertEqual(state['parser_alerted_status'], 'healthy')
|
||||
|
||||
def test_failed_alert_is_retried_on_next_run(self):
|
||||
state = {}
|
||||
errors = ['naver_api: timeout', '38_fallback: timeout']
|
||||
with patch.object(sync, 'send_telegram', side_effect=[False, True]) as send:
|
||||
sync.update_parser_health(state, 'failed', errors, 0)
|
||||
sync.update_parser_health(state, 'failed', errors, 0)
|
||||
|
||||
self.assertEqual(send.call_count, 2)
|
||||
self.assertEqual(state['parser_alerted_status'], 'failed')
|
||||
|
||||
def test_primary_parser_gets_same_day_listing_augmentation(self):
|
||||
future = sync.EventSpec(
|
||||
'subscription', '넥스아이', date(2026, 11, 3), date(2026, 11, 4),
|
||||
'한국투자증권', 'https://stock.naver.com/ipo/A493410',
|
||||
)
|
||||
listing_html = '''
|
||||
<table summary="신규상장종목"><tbody>
|
||||
<tr><th>기업명</th><th>신규상장일</th></tr>
|
||||
<tr><td>빅웨이브로보틱스</td><td>2026/09/29</td></tr>
|
||||
</tbody></table>'''
|
||||
with patch.object(sync, 'parse_naver_api_events_from', return_value=[future]), patch.object(
|
||||
sync, 'fetch', return_value=listing_html
|
||||
):
|
||||
events, parser_used, errors = sync.collect_ipo_events(date(2026, 9, 29))
|
||||
|
||||
self.assertEqual(parser_used, 'naver_api')
|
||||
self.assertEqual(errors, [])
|
||||
self.assertEqual({e.state_key for e in events}, {
|
||||
'subscription|넥스아이', 'listing|빅웨이브로보틱스',
|
||||
})
|
||||
|
||||
def test_primary_parser_backfills_recent_non_spac_listing(self):
|
||||
future = sync.EventSpec(
|
||||
'subscription', '넥스아이', date(2026, 11, 3), date(2026, 11, 4),
|
||||
'한국투자증권', 'https://stock.naver.com/ipo/A493410',
|
||||
)
|
||||
listing_html = '''
|
||||
<table summary="신규상장종목"><tbody>
|
||||
<tr><th>기업명</th><th>신규상장일</th></tr>
|
||||
<tr><td>와이즈플래닛컴퍼니</td><td>2026/09/23</td></tr>
|
||||
<tr><td>KB스팩34호</td><td>2026/09/22</td></tr>
|
||||
<tr><td>덕산넵코어스(구.넵코어스)</td><td>2026/09/30</td></tr>
|
||||
</tbody></table>'''
|
||||
with patch.object(sync, 'parse_naver_api_events_from', return_value=[future]), patch.object(
|
||||
sync, 'fetch', return_value=listing_html
|
||||
):
|
||||
events, parser_used, errors = sync.collect_ipo_events(date(2026, 9, 29))
|
||||
|
||||
self.assertEqual(parser_used, 'naver_api')
|
||||
self.assertEqual(errors, [])
|
||||
self.assertIn('listing|와이즈플래닛컴퍼니', {e.state_key for e in events})
|
||||
self.assertNotIn('listing|KB스팩34호', {e.state_key for e in events})
|
||||
self.assertNotIn(
|
||||
'listing|덕산넵코어스(구.넵코어스)', {e.state_key for e in events}
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user