Files
ai-trade/services/naver-fetch/tests/test_fetch_worker.py

108 lines
3.7 KiB
Python

"""fetch_worker — 주간창/조립/격리/heartbeat."""
import datetime as dt
import json
from zoneinfo import ZoneInfo
from fetch_worker import FetchState, in_daytime, run_cycle, build_heartbeat
from config import load_settings
KST = ZoneInfo("Asia/Seoul")
def _kst(hh, mm):
return dt.datetime(2026, 7, 10, hh, mm, tzinfo=KST)
def test_in_daytime_boundaries():
assert in_daytime(_kst(8, 0), "08:00", "20:00") is True
assert in_daytime(_kst(19, 59), "08:00", "20:00") is True
assert in_daytime(_kst(20, 0), "08:00", "20:00") is False
assert in_daytime(_kst(7, 59), "08:00", "20:00") is False
class _FakeNAS:
def __init__(self, targets):
self._targets = targets
self.ingests = [] # 동별 post_ingest 호출 기록
async def get_targets(self):
return self._targets
async def post_ingest(self, fetched_at, batches):
self.ingests.append({"fetched_at": fetched_at, "batches": batches})
n = sum(len(b["articles"]) for b in batches)
return {"received": n, "new": n, "queued": True}
class _FakeNaver:
def __init__(self, fail_dongs=None):
self._fail = fail_dongs or set()
self.token_acquired = False
async def acquire_token(self):
self.token_acquired = True
async def fetch_articles(self, cortar_no, page=1):
if cortar_no in self._fail:
raise RuntimeError("naver 429")
return {"isMoreData": False,
"articleList": [{"articleNo": f"{cortar_no}-{page}"}]}
async def test_run_cycle_ingests_per_dong():
nas = _FakeNAS({"dongs": [{"dong": "신대방동", "cortar_no": "1159010100"},
{"dong": "봉천동", "cortar_no": "1162010100"}],
"deal_types": ["매매"], "page_limit": 1})
naver = _FakeNaver()
state = FetchState()
await run_cycle(nas, naver, state, load_settings())
assert naver.token_acquired is True
assert len(nas.ingests) == 2 # 동별 1 POST씩
dongs = [ing["batches"][0]["dong"] for ing in nas.ingests]
assert dongs == ["신대방동", "봉천동"]
assert all(len(ing["batches"]) == 1 for ing in nas.ingests)
assert state.listings_pushed == 2
assert state.last_fetch_at is not None
assert state.state == "idle"
assert nas.ingests[0]["fetched_at"].endswith("Z")
async def test_run_cycle_dong_failure_isolated():
nas = _FakeNAS({"dongs": [{"dong": "A", "cortar_no": "1159010100"},
{"dong": "B", "cortar_no": "1162010100"}],
"page_limit": 1})
naver = _FakeNaver(fail_dongs={"1159010100"})
state = FetchState()
await run_cycle(nas, naver, state, load_settings())
# A는 fetch 실패 → ingest skip, B만 ingest (부분 실패 격리)
assert len(nas.ingests) == 1
assert nas.ingests[0]["batches"][0]["dong"] == "B"
assert state.errors >= 1
async def test_run_cycle_token_failure_skips_ingest():
class _BadNaver(_FakeNaver):
async def acquire_token(self):
raise RuntimeError("token 실패")
nas = _FakeNAS({"dongs": [], "page_limit": 1})
state = FetchState()
await run_cycle(nas, _BadNaver(), state, load_settings())
assert nas.ingests == []
assert state.errors == 1
assert state.state == "idle"
def test_build_heartbeat_schema():
state = FetchState()
state.state = "fetching"
state.listings_pushed = 7
d = json.loads(build_heartbeat(state))
assert d["name"] == "naver-fetch"
assert d["kind"] == "fetcher"
assert d["state"] == "fetching"
assert isinstance(d["ts"], int)
assert d["last_fetch_at"] is None
assert d["listings_pushed"] == 7
assert d["errors"] == 0