支持其他店铺
This commit is contained in:
@@ -25,6 +25,14 @@ class TestUrls:
|
||||
)
|
||||
# 空字符串 tenpo_cd 与未提供等价(与迁移前 if tenpo_cd 判断一致)
|
||||
assert product_detail_url("128001234", tenpo_cd="") == "https://www.suruga-ya.jp/product/detail/128001234"
|
||||
assert (
|
||||
product_detail_url("128001234", tenpo_cd="400496", branch_number="0003")
|
||||
== "https://www.suruga-ya.jp/product/detail/128001234?tenpo_cd=400496&branch_number=0003"
|
||||
)
|
||||
assert (
|
||||
product_detail_url("128001234", branch_number="0003")
|
||||
== "https://www.suruga-ya.jp/product/detail/128001234?branch_number=0003"
|
||||
)
|
||||
|
||||
def test_trade_detail_url(self):
|
||||
assert (
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
"""SurugayaClient._parse_other_shop_list 的解析单元测试。
|
||||
|
||||
固定 HTML 节选自真实的 /product/other/{goods_id} 页面结构(tbl_all 标签页),
|
||||
覆盖官方自营行(无 tenpo_cd/无评分)与加盟店/市场店铺行(含 tenpo_cd/评分)。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from selectolax.parser import HTMLParser
|
||||
|
||||
from app.services.surugaya_client import SurugayaClient
|
||||
|
||||
OTHER_SHOP_HTML = """
|
||||
<div class="tab-item" id="tabs-all">
|
||||
<table id="tbl_all">
|
||||
<tr class="thead"><th>头</th></tr>
|
||||
<tr class="item">
|
||||
<td><p><strong class="text-red text-bold mgnL10">1,370円</strong></p></td>
|
||||
<td>
|
||||
<a href="/product/detail/220035970?tenpo_cd=400451&branch_number=0003">
|
||||
<h2 class="title_product">中古箱・ジャケット・ケース不備(中)</h2>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<div class="space_text_1 mgnB5"><strong><a href="/shop/400451">駿河屋 山口大学前店</a></strong></div>
|
||||
<div class="star_group mgnB5"></div>
|
||||
<div>5.0(1334件)</div>
|
||||
</td>
|
||||
<td>
|
||||
<ul class="shipping_campaing_info">
|
||||
<li class="padT5">1日〜3日以内に発送します</li>
|
||||
<li class="all ajax-campaign-placeholder"
|
||||
data-zaiko_data="{"shinaban":"220035970","branch_number":"0003","tenpo_cd":"400451","baika":"1370"}">
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="item">
|
||||
<td><p><strong class="text-red text-bold mgnL10">2,000円</strong></p></td>
|
||||
<td>
|
||||
<a href="/product/detail/220035970?branch_number=0001">
|
||||
<h2 class="title_product">中古</h2>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<div><a href="/"><img src="/database/images/top-logo.png" class="w100"></a></div>
|
||||
</td>
|
||||
<td>
|
||||
<ul class="shipping_campaing_info">
|
||||
<li class="padT5">1日〜7日以内に発送します</li>
|
||||
<li class="all ajax-campaign-placeholder"
|
||||
data-zaiko_data="{"shinaban":"220035970","branch_number":"0001","tenpo_cd":"","baika":"2000"}">
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="item">
|
||||
<td><p><strong class="text-red text-bold mgnL10">2,744円</strong></p></td>
|
||||
<td>
|
||||
<a href="/product/detail/220035970?tenpo_cd=410118&branch_number=9001">
|
||||
<h2 class="title_product">新品新古品(店頭在庫品) ※ショップ情報をご確認下さい</h2>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<div class="space_text_1 mgnB5"><strong><a href="/shop/410118">玉光堂 駿河屋マーケットプレイス店</a></strong></div>
|
||||
<div class="star_group mgnB5"></div>
|
||||
<div>5.0(46件)</div>
|
||||
</td>
|
||||
<td>
|
||||
<ul class="shipping_campaing_info">
|
||||
<li class="padT5">2日〜7日以内に発送します</li>
|
||||
<li class="all ajax-campaign-placeholder"
|
||||
data-zaiko_data="{"shinaban":"220035970","branch_number":"9001","tenpo_cd":"410118","baika":"2744"}">
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
def test_parse_other_shop_list_extracts_all_rows() -> None:
|
||||
"""三行分别为:加盟店、官方自营、市场店铺,均应被正确解析。"""
|
||||
tree = HTMLParser(OTHER_SHOP_HTML)
|
||||
items = SurugayaClient._parse_other_shop_list(tree)
|
||||
|
||||
assert len(items) == 3
|
||||
|
||||
|
||||
def test_parse_other_shop_list_franchise_row() -> None:
|
||||
"""加盟店行:tenpo_cd/店名/评分/价格/发货时效均应正确填充。"""
|
||||
tree = HTMLParser(OTHER_SHOP_HTML)
|
||||
item = SurugayaClient._parse_other_shop_list(tree)[0]
|
||||
|
||||
assert item.tenpo_cd == "400451"
|
||||
assert item.is_official is False
|
||||
assert item.shop_name == "駿河屋 山口大学前店"
|
||||
assert item.shop_url == "https://www.suruga-ya.jp/shop/400451"
|
||||
assert item.price == 1370
|
||||
assert item.condition == "中古箱・ジャケット・ケース不備(中)"
|
||||
assert item.branch_number == "0003"
|
||||
assert item.rating_score == "5.0"
|
||||
assert item.rating_count == 1334
|
||||
assert item.shipping_note == "1日〜3日以内に発送します"
|
||||
assert item.detail_url == (
|
||||
"https://www.suruga-ya.jp/product/detail/220035970?tenpo_cd=400451&branch_number=0003"
|
||||
)
|
||||
|
||||
|
||||
def test_parse_other_shop_list_official_row_has_no_tenpo_cd() -> None:
|
||||
"""官方自营行:无 /shop/ 链接、tenpo_cd 为空、is_official 应为 True,且无评分。"""
|
||||
tree = HTMLParser(OTHER_SHOP_HTML)
|
||||
item = SurugayaClient._parse_other_shop_list(tree)[1]
|
||||
|
||||
assert item.tenpo_cd == ""
|
||||
assert item.is_official is True
|
||||
assert item.shop_name == "駿河屋"
|
||||
assert item.price == 2000
|
||||
assert item.rating_score == ""
|
||||
assert item.rating_count == 0
|
||||
|
||||
|
||||
def test_parse_other_shop_list_marketplace_row() -> None:
|
||||
"""市场店铺行:与加盟店行结构一致,同样应正确解析。"""
|
||||
tree = HTMLParser(OTHER_SHOP_HTML)
|
||||
item = SurugayaClient._parse_other_shop_list(tree)[2]
|
||||
|
||||
assert item.tenpo_cd == "410118"
|
||||
assert item.is_official is False
|
||||
assert item.shop_name == "玉光堂 駿河屋マーケットプレイス店"
|
||||
assert item.price == 2744
|
||||
assert item.rating_score == "5.0"
|
||||
assert item.rating_count == 46
|
||||
|
||||
|
||||
def test_parse_other_shop_list_empty_html_returns_empty_list() -> None:
|
||||
"""页面结构不包含 tbl_all 时(如解析失败/改版),应返回空列表而非抛异常。"""
|
||||
tree = HTMLParser("<html><body>no table here</body></html>")
|
||||
assert SurugayaClient._parse_other_shop_list(tree) == []
|
||||
@@ -0,0 +1,61 @@
|
||||
"""SurugayaClient 中「指定店铺」相关能力的单元测试:
|
||||
|
||||
- _build_product_url 携带 tenpo_cd/branch_number 构造跳转到指定店铺报价的详情页 URL
|
||||
- fetch_other_shops 独立于 fetch_detail,仅返回 other_shop_list(解耦双倍抓取成本)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from app.core.config import Settings
|
||||
from app.models.scrape import DetailRequest, OtherShopItem
|
||||
from app.services.surugaya_client import SurugayaClient
|
||||
|
||||
|
||||
def _make_client() -> SurugayaClient:
|
||||
return SurugayaClient(
|
||||
settings=Settings(_env_file=None),
|
||||
browser_pool=None,
|
||||
session_manager=None,
|
||||
session_store=None,
|
||||
)
|
||||
|
||||
|
||||
class TestBuildProductUrl:
|
||||
def test_without_shop_params(self) -> None:
|
||||
client = _make_client()
|
||||
assert client._build_product_url("220035970") == "https://www.suruga-ya.jp/product/detail/220035970"
|
||||
|
||||
def test_with_tenpo_cd_and_branch_number(self) -> None:
|
||||
client = _make_client()
|
||||
url = client._build_product_url("220035970", tenpo_cd="400451", branch_number="0003")
|
||||
assert url == "https://www.suruga-ya.jp/product/detail/220035970?tenpo_cd=400451&branch_number=0003"
|
||||
|
||||
def test_missing_product_id_raises(self) -> None:
|
||||
client = _make_client()
|
||||
with pytest.raises(Exception):
|
||||
client._build_product_url(None)
|
||||
|
||||
|
||||
class TestFetchOtherShops:
|
||||
@pytest.mark.asyncio
|
||||
async def test_returns_goods_id_and_shop_list(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
client = _make_client()
|
||||
fake_items = [OtherShopItem(tenpo_cd="400451", shop_name="駿河屋 山口大学前店", price=1370)]
|
||||
|
||||
async def fake_fetch_other_shop_list(goods_id: str) -> list[OtherShopItem]:
|
||||
assert goods_id == "220035970"
|
||||
return fake_items
|
||||
|
||||
monkeypatch.setattr(client, "_fetch_other_shop_list", fake_fetch_other_shop_list)
|
||||
|
||||
result = await client.fetch_other_shops(DetailRequest(id="220035970"))
|
||||
|
||||
assert result.goods_id == "220035970"
|
||||
assert result.other_shop_list == fake_items
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_empty_id_raises(self) -> None:
|
||||
client = _make_client()
|
||||
with pytest.raises(Exception):
|
||||
await client.fetch_other_shops(DetailRequest(id=None))
|
||||
Reference in New Issue
Block a user