# 쿠팡 파서 결정론적 단위 테스트(네트워크/브라우저 불필요). # fixture 는 실제 렌더된 검색결과에서 카드 4개를 추출한 것(단위가격 '1개당' 케이스 포함). from pathlib import Path from services.search.coupang.parser import parse_search_html from services.search.coupang.adapter import detect_block FIXTURE = Path(__file__).parent / "fixtures" / "coupang_search.html" def test_parse_extracts_valid_products(): items = parse_search_html(FIXTURE.read_text()) assert len(items) >= 3 for p in items: assert p.source == "coupang" assert p.name and len(p.name) > 2 # 단위가격('1개당 44,400원')의 '1' 을 가격으로 오인하던 회귀 방지 assert p.price >= 100, f"이상 저가: {p.price} ({p.name})" assert p.detail_url and p.detail_url.startswith("https://www.coupang.com/vp/products/") assert p.external_id # vendorItemId(data-id) def test_parse_price_is_sale_not_unit_price(): # fixture 첫 카드의 판매가는 44,400원(단위가격도 '1개당 44,400원'이라 값은 같지만 # 파서가 정가(del)나 '%'가 아닌 판매가를 정확히 집는지 확인) items = parse_search_html(FIXTURE.read_text()) assert items[0].price == 44400 def test_parse_shipping_from_fixture(): # fixture 4카드: [0,1]=로켓뱃지+무료배송, [2]=무료배송 텍스트만, [3]=판매자로켓 뱃지만 items = parse_search_html(FIXTURE.read_text()) assert (items[0].shipping_fee, items[0].shipping_type) == (0, "rocket") assert (items[1].shipping_fee, items[1].shipping_type) == (0, "rocket") assert (items[2].shipping_fee, items[2].shipping_type) == (0, "free") # 판매자로켓은 조건부 무료 → 명시 텍스트 없으면 배송비 미확인(None) assert (items[3].shipping_fee, items[3].shipping_type) == (None, "rocket_merchant") def _card(name: str, price: str, extra: str = "") -> str: return f"""
  • {name}
    {name}
    {price}
    {extra}
  • """ def test_parse_shipping_paid_fee(): # 명시 배송비('배송비 3,000원') → paid + 금액 html = f"" 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"" 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