쿠팡 크롤 IP 를 '막힐 때까지' 쓰던 방식을 '막히기 전에 교체'로 전환한다. - 요청 예산(LPS_IP_REQUEST_BUDGET, 기본 3): IP당 요청 수가 예산에 닿으면 차단 전에 선제 회전. 실측상 5회 부근 차단 이력이 있어 보수적으로 3회. 선제 교체된 포트는 평판이 깨끗해 로테이션 복귀 시 재사용된다. - 포트 쿨다운(LPS_PORT_COOLDOWN_SEC, 기본 max(sticky,30분)): 차단 감지· 전송오류 포트는 격리하고 _port() 가 건너뛴다. 전 포트 쿨다운이면 만료 임박 포트 사용(가용성 우선). 포트 수는 config 범위에서 동적 산출. - 차단 재시도 소진 시에도 회전 예약 — 불탄 포트로 다음 검색을 하지 않음. - ip_session 테이블 신설: 세션마다 요청 수·성공/차단·종료 사유(budget/ block/proxy_error/window/idle/shutdown)를 기록. bot_detection 과 달리 무사 종료도 남아 예산 상한 튜닝의 원천 데이터가 된다(쿼리 database.md). models.py·migrations·init.sql(lps_db 섹션) 동행 갱신, dev DB 적용 완료. - 테스트 17건 추가(쿨다운·예산 판정·세션 기록·CRUD), 전체 126 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
88 lines
3.2 KiB
Python
88 lines
3.2 KiB
Python
"""DECODO 프록시 제공자 테스트 (포트 기반 sticky, 순수·네트워크 불필요)."""
|
|
|
|
from config.config_models import DecodoConfig
|
|
from services.search.proxy import DecodoProxy
|
|
|
|
|
|
def _p(**kw):
|
|
base = dict(host="gate.decodo.com", username="user1", password="pw", port_start=10001, port_end=10010, session_minutes=10)
|
|
base.update(kw)
|
|
return DecodoProxy(DecodoConfig(**base))
|
|
|
|
|
|
def test_disabled_when_credentials_missing():
|
|
assert DecodoProxy(DecodoConfig()).enabled is False # 전부 비어있음
|
|
assert _p(password="").enabled is False
|
|
assert _p(port_end=0).enabled is False # 포트 미설정
|
|
assert _p().enabled is True
|
|
|
|
|
|
def test_playwright_proxy_shape():
|
|
cfg = _p().playwright_proxy()
|
|
assert cfg["username"] == "user1" and cfg["password"] == "pw" # 고정 자격증명
|
|
assert cfg["server"].startswith("http://gate.decodo.com:")
|
|
port = int(cfg["server"].rsplit(":", 1)[1])
|
|
assert 10001 <= port <= 10010 # 포트 범위 안
|
|
|
|
|
|
def test_disabled_returns_none():
|
|
assert DecodoProxy(DecodoConfig()).playwright_proxy() is None
|
|
|
|
|
|
def test_port_selected_within_range_and_stable_in_window():
|
|
p = _p()
|
|
port = p._port()
|
|
assert 10001 <= port <= 10010
|
|
assert p._port() == port # 같은 시간창에서는 동일 포트(동일 sticky IP)
|
|
|
|
|
|
def test_single_port_range():
|
|
p = _p(port_start=10001, port_end=10001)
|
|
assert p._port() == 10001 # 포트 1개면 항상 그 포트
|
|
|
|
|
|
def test_rotate_advances_port_immediately():
|
|
p = _p(port_start=10001, port_end=10003) # 포트 3개
|
|
before = p._port()
|
|
p.rotate()
|
|
after = p._port()
|
|
assert after != before # 즉시 다음 포트(새 IP)
|
|
assert 10001 <= after <= 10003
|
|
p.rotate(); p.rotate() # 3번 회전하면 한 바퀴 → 원위치
|
|
assert p._port() == before
|
|
|
|
|
|
def test_burned_port_is_skipped():
|
|
p = _p(port_start=10001, port_end=10003)
|
|
burned = p._port()
|
|
p.mark_burned(burned)
|
|
assert p._port() != burned # 쿨다운 중인 포트는 건너뜀
|
|
assert p.available_ports() == 2
|
|
|
|
|
|
def test_burned_port_returns_after_cooldown():
|
|
p = _p(port_start=10001, port_end=10003)
|
|
burned = p._port()
|
|
p.mark_burned(burned, cooldown_sec=0) # 즉시 만료
|
|
assert p._port() == burned # 만료 후엔 다시 사용 가능
|
|
assert p.available_ports() == 3
|
|
|
|
|
|
def test_all_burned_falls_back_to_earliest_expiry():
|
|
p = _p(port_start=10001, port_end=10002) # 포트 2개
|
|
first, second = 10001, 10002
|
|
p.mark_burned(first, cooldown_sec=10) # 먼저 만료
|
|
p.mark_burned(second, cooldown_sec=999)
|
|
assert p._port() == first # 전 포트 쿨다운 — 만료 임박 포트 사용(가용성 우선)
|
|
|
|
|
|
def test_mark_burned_ignores_none_port():
|
|
p = _p()
|
|
p.mark_burned(None) # 프록시 미사용 세션 — no-op
|
|
assert p.available_ports() == 10
|
|
|
|
|
|
def test_cooldown_default_at_least_30min():
|
|
assert _p(session_minutes=10).cooldown_sec == 1800 # max(sticky 600, 1800)
|
|
assert _p(session_minutes=60).cooldown_sec == 3600 # sticky 가 더 길면 sticky 만큼
|