"""乐天商家(店铺)解析与请求转换测试""" import json from pathlib import Path import pytest from app.shared.errors import InvalidRequestError, ScrapeParseError from app.scraping.models.scrape import ShopItemsRequest, SortOption from app.scraping.parsers.shop import parse_shop_detail from app.scraping.utils.urls import build_search_url, build_shop_url, split_shop_url FIXTURES = Path(__file__).parent / "fixtures" @pytest.fixture def shop_state() -> dict: return json.loads((FIXTURES / "shop_state.json").read_text(encoding="utf-8")) @pytest.fixture def shop_html() -> str: return (FIXTURES / "shop_page.html").read_text(encoding="utf-8") # ---- 解析 ---- def test_parse_shop_detail_reads_state_and_logo(shop_state, shop_html): detail = parse_shop_detail(shop_state, shop_code="edion", html=shop_html) assert detail.shop_id == 272415 assert detail.shop_code == "edion" assert detail.shop_name == "エディオン 楽天市場店" assert detail.review_score == 4.51 assert detail.review_count == 128136 assert detail.review_displayed is True assert detail.is_39_shop is True assert detail.status == 1 assert detail.introduction assert detail.signboard_url.startswith("https://") # logo 不在 state 里,只能从页面的 ld+json 微数据取 assert detail.logo_url.startswith("https://thumbnail.image.rakuten.co.jp/") def test_parse_shop_detail_normalizes_shop_url(shop_state): """state 里的 shopUrl 是 http 且不带尾斜杠,对外统一成规范形式""" detail = parse_shop_detail(shop_state, shop_code="edion") assert detail.shop_url == "https://www.rakuten.co.jp/edion/" def test_parse_shop_detail_without_html_leaves_logo_empty(shop_state): assert parse_shop_detail(shop_state, shop_code="edion").logo_url == "" def test_parse_shop_detail_rejects_page_without_shop_node(): with pytest.raises(ScrapeParseError): parse_shop_detail({"state": {}}, shop_code="edion") # ---- URL ---- def test_shop_url_round_trip(): assert split_shop_url(build_shop_url("edion")) == "edion" def test_split_shop_url_rejects_reserved_paths(): """/category/ 之类的站点自有路径不是店铺页,别把 category 当成店铺代码""" with pytest.raises(InvalidRequestError): split_shop_url("https://www.rakuten.co.jp/category/101205/") def test_split_shop_url_rejects_foreign_host(): with pytest.raises(InvalidRequestError): split_shop_url("https://item.rakuten.co.jp/edion/123/") # ---- 商家商品:转成限定店铺的搜索 ---- def test_shop_items_request_converts_to_shop_scoped_search(): payload = ShopItemsRequest( shop_code="edion", page=2, keyword="テレビ", sort=SortOption.PRICE_ASC, max_price=50000 ) search = payload.to_search_request(272415) assert search.shop_id == 272415 assert search.keyword == "テレビ" assert search.page == 2 url = build_search_url(search) assert "sid=272415" in url assert "p=2" in url assert "s=2" in url # price_asc assert "max=50000" in url def test_shop_items_request_needs_an_identifier(): with pytest.raises(ValueError): ShopItemsRequest() def test_search_accepts_shop_id_alone_as_target(): """只按店铺取商品是合法查询:站点的 sid-only 搜索能正常返回""" url = build_search_url(ShopItemsRequest(shop_id=272415).to_search_request(272415)) assert "sid=272415" in url