"
p = parse_search_html(html)[0]
assert (p.shipping_fee, p.shipping_type) == (3000, "paid")
def test_parse_shipping_name_free_text_no_false_positive():
# 상품명에 '무료배송' 이 들어가도 배송 무료로 오인하지 않는다
html = f"
{_card('무료배송 텀블러 굿즈', '12,000원')}
"
p = parse_search_html(html)[0]
assert (p.shipping_fee, p.shipping_type) == (None, None)
# --- 차단 감지(detect_block): 여러 flavor 를 모두 blocked 로 잡아야 IP 회전이 걸린다 ---
def test_detect_block_none_when_products_exist():
# 상품이 파싱됐으면 HTML 내용과 무관하게 차단 아님
assert detect_block("Access Denied", product_count=3) is None
def test_detect_block_akamai_challenge():
assert detect_block("
" + "x" * 20000, 0) == "sec-if-cpt-container"
def test_detect_block_edge_access_denied():
# 실제로 잡힌 303B edge deny
html = ('Access Denied
Access Denied
'
"You don't have permission to access ... errors.edgesuite.net")
assert detect_block(html, 0) in ("errors.edgesuite.net", "You don't have permission to access")
def test_detect_block_permission_restricted_page():
html = "쿠팡을 찾아주신 고객님, 입력하신 페이지주소는 사용권한이 제한된 페이지입니다."
assert detect_block(html, 0) in ("사용권한이 제한된", "쿠팡을 찾아주신 고객님")
def test_detect_block_short_html_fallback():
# 마커 없어도 비정상적으로 짧은 0건 응답은 미지의 차단으로 폴백
marker = detect_block("oops", 0)
assert marker is not None and marker.startswith("short_html(")
def test_detect_block_genuine_empty_large_page_not_blocked():
# 정상 '검색결과 없음'(전체 chrome 포함, 큰 HTML)은 차단 아님 → not_found 로 흘러야 함
big = "검색결과가 없습니다" + "x" * 20000 + ""
assert detect_block(big, 0) is None
def test_trust_signals_are_captured():
"""쿠팡은 별점이 채워진 별 개수가 아니라 컨테이너 aria-label 에, 리뷰 수는 괄호 텍스트에 있다."""
ps = parse_search_html(FIXTURE.read_text())
rated = [p for p in ps if p.review_count]
assert rated
assert all(0 < p.rating <= 5 for p in rated)
assert max(p.review_count for p in rated) > 1000 # '41,448' 처럼 콤마가 섞여도 파싱된다
def test_shipping_label_joins_smallest_fragments():
"""쿠팡 배송 문구는 유틸리티 클래스에 담겨 셀렉터로 못 집는다 — 텍스트 조각을 합쳐 만든다.
부모는 구분자 없이 붙어 나오므로("내일(목) 도착무료배송") 버리고, 조각만 이어야 한다."""
ps = parse_search_html(FIXTURE.read_text())
labels = [p.shipping_label for p in ps if p.shipping_label]
assert labels, "배송 표기가 하나도 안 잡히면 문구 패턴이 드리프트한 것"
assert any("도착" in x and "무료배송" in x for x in labels)
# 조각이 뭉개지지 않았는지 — '도착'만 남으면 언제 오는지가 사라진다
assert any(re.search(r"(내일|모레|오늘)\(?\w*\)? 도착", x) for x in labels)