Files
web-page-backend/realestate-lab/tests/test_internal_ingest.py
gahusb f676116336 fix(realestate): 네이버 매물 dong_code를 dong명에서 유도(cortarNo 부재)→ market_deals 매칭·판정 복구
네이버 article에 cortarNo가 없는 경우 listing.dong_code가 None으로 저장되어
get_market_deals_for가 시세를 못 찾아 모든 매물이 '보류'(판정 불가) 상태였다.
dong명은 항상 정확히 태깅되므로 lawd_code(dong)으로 5자리 시군구코드를 유도해
market_deals와 매칭시킨다.

- internal_router.listings_ingest: dong_code 없으면 batch.dong에서 lawd_code로 보정
- db.upsert_listing: ON CONFLICT UPDATE SET에 dong_code 추가(재유입 시 기존 None행 갱신)
- listing_matcher.run_listing_matching: get_market_deals_for 호출 전 dong_code 없으면
  listing.dong에서 즉시 유도(기존 저장된 None행도 재실행만으로 판정 복구)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EqCYBhvTcdeCTUDX3RhWx9
2026-07-10 14:11:51 +09:00

82 lines
4.1 KiB
Python

import os, importlib
from unittest.mock import patch
from fastapi.testclient import TestClient
def _client(monkeypatch):
monkeypatch.setenv("INTERNAL_API_KEY", "SECRET")
from app import auth as _a
importlib.reload(_a)
from app.main import app
return TestClient(app)
_ARTICLE = {"articleNo": "N100", "tradeTypeName": "전세", "dealOrWarrantPrc": "2억 9,000",
"area2": "42.5", "floorInfo": "5/15", "articleName": "OO아파트",
"buildingName": "OO", "cortarNo": "1159010100"}
def test_ingest_auth(monkeypatch):
c = _client(monkeypatch)
# 헤더 없음 — FastAPI Header(...)는 dependency 실행 전 자체 검증하므로 422.
# test_internal_targets.py의 동일 패턴과 일치(같은 서비스 선례).
assert c.post("/api/internal/realestate/listings-ingest", json={"batches": []}).status_code in (401, 422)
assert c.post("/api/internal/realestate/listings-ingest",
headers={"X-Internal-Key": "WRONG"}, json={"batches": []}).status_code == 401
def test_ingest_upserts_and_matches(monkeypatch):
c = _client(monkeypatch)
# TestClient는 BackgroundTask를 응답 반환 전 동기 실행하므로 patch된 notify는
# c.post()가 리턴하는 시점에 이미 호출되어 있다.
with patch("app.internal_router.notify_new_listings", return_value={"sent": 1, "sent_ids": [1]}) as noti:
r = c.post("/api/internal/realestate/listings-ingest",
headers={"X-Internal-Key": "SECRET"},
json={"fetched_at": "2026-07-09T00:00:00Z",
"batches": [{"dong": "신대방동", "articles": [_ARTICLE]}]})
assert r.status_code == 200
body = r.json()
assert body["received"] == 1 and body["new"] == 1 and body["queued"] is True
assert noti.call_count == 1
# 저장 확인
from app import db
ls = db.get_listings()
assert any(l["article_no"] == "N100" and l["dong"] == "신대방동" for l in ls)
def test_ingest_idempotent(monkeypatch):
c = _client(monkeypatch)
payload = {"batches": [{"dong": "신대방동", "articles": [_ARTICLE]}]}
with patch("app.internal_router.notify_new_listings", return_value={"sent": 0}):
c.post("/api/internal/realestate/listings-ingest", headers={"X-Internal-Key": "SECRET"}, json=payload)
r2 = c.post("/api/internal/realestate/listings-ingest", headers={"X-Internal-Key": "SECRET"}, json=payload)
assert r2.json()["new"] == 0 # 재push → 신규 0
def test_ingest_holds_match_notify_lock(monkeypatch):
from app import internal_router as ir
from app.pipeline_lock import match_notify_lock
c = _client(monkeypatch)
holds = {}
def fake_notify():
holds["locked"] = match_notify_lock.locked()
return {"sent": 0}
monkeypatch.setattr(ir, "notify_new_listings", fake_notify)
c.post("/api/internal/realestate/listings-ingest", headers={"X-Internal-Key": "SECRET"},
json={"batches": [{"dong": "신대방동", "articles": [_ARTICLE]}]})
assert holds["locked"] is True # 알림 시점에 락 보유(임계구역 직렬화 증명)
# ── 근본원인 fix: 네이버 article에 cortarNo 없으면 dong_code=None → 시세 매칭 불가 ──
_ARTICLE_NO_CORTAR = {"articleNo": "N200", "tradeTypeName": "전세", "dealOrWarrantPrc": "2억 9,000",
"area2": "42.5", "floorInfo": "5/15", "articleName": "OO아파트",
"buildingName": "OO"} # cortarNo 없음 — 실제 네이버 응답 케이스
def test_ingest_derives_dong_code_from_dong_when_cortarno_missing(monkeypatch):
c = _client(monkeypatch)
with patch("app.internal_router.notify_new_listings", return_value={"sent": 0}):
r = c.post("/api/internal/realestate/listings-ingest",
headers={"X-Internal-Key": "SECRET"},
json={"batches": [{"dong": "신길동", "articles": [_ARTICLE_NO_CORTAR]}]})
assert r.status_code == 200
from app import db
ls = db.get_listings()
saved = next(l for l in ls if l["article_no"] == "N200")
assert saved["dong_code"] == "11560" # 신길동 → 영등포구 5자리 lawd_code로 유도