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로 유도