- docker-compose.yml realestate-lab environment에 INTERNAL_API_KEY/NAVER_PAGE_LIMIT 누락 추가 (verify_internal_key 항상 401 → 워커 계약 전체 마비 방지, sibling image/video/music/insta-lab과 동형) - pipeline_lock.py 신설: cron _run_listing_pipeline과 워커 listings_ingest가 run_listing_matching()+notify_new_listings() 임계구역을 공유 threading.Lock으로 직렬화 (동시 실행 시 동일 매물 중복 텔레그램 발송 방지). 느린 collect는 락 밖 유지. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqCYBhvTcdeCTUDX3RhWx9
60 lines
2.9 KiB
Python
60 lines
2.9 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)
|
|
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["matched"] == 1
|
|
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 # 알림 시점에 락 보유(임계구역 직렬화 증명)
|