Ctrl K
ComunidadGuías…

tests/test_site_icons.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""站点图标发现:把候选找齐,再按「合适」而不是只按「大」来排。

这些用例的形态全部来自 2026-09-02 对真实站点的实测,注释里写的是哪一个站逼出了这一条。
"""
import sys
import unittest
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))

from peach import site_icons, social_links   # noqa: E402


def urls(candidates):
    return [c.url for c in candidates]


class LinkParsingTests(unittest.TestCase):
    def test_sizes_and_relative_hrefs_are_resolved(self):
        html = ('<link rel="apple-touch-icon" sizes="180x180" href="/a/touch.png">'
                '<link rel="shortcut icon" href="favicon.ico">')
        found = site_icons.link_candidates(html, "https://example.com/")
        self.assertEqual(urls(found), ["https://example.com/a/touch.png",
                                       "https://example.com/favicon.ico"])
        self.assertEqual([c.size for c in found], [180, site_icons.DEFAULT_SIZE])

    def test_a_multi_valued_sizes_attribute_takes_the_largest(self):
        """`sizes="180x180 152x152 120x120"` 是一个文件里装多份,取最大的那份记分。"""
        html = '<link rel="apple-touch-icon" sizes="120x120 180x180" href="/t.png">'
        self.assertEqual(site_icons.link_candidates(html, "https://e.com/")[0].size, 180)

    def test_an_svg_outranks_every_bitmap_regardless_of_rel(self):
        """threads.com 的实测形态,也是这次改动的起点。

        它把 512 viewBox 的成品 SVG 声明成 `rel="icon"`,同时根目录还躺着一个
        180×180 的 `apple-touch-icon.png`。按 `rel` 分层会让位图赢,那就又回到了
        「抓到一个就不找别的了」。
        """
        html = ('<link rel="apple-touch-icon" sizes="180x180" href="/touch.png">'
                '<link rel="icon" type="image/svg+xml" href="/mark.svg">')
        ranked = site_icons.rank(site_icons.link_candidates(html, "https://t.com/"))
        self.assertEqual(ranked[0].url, "https://t.com/mark.svg")
        self.assertTrue(ranked[0].vector)

    def test_a_mask_icon_is_kept_but_ranked_last(self):
        """`rel="mask-icon"` 按规范是纯黑剪影。

        当成品图标用会得到一枚全黑方块,所以不能排前面;但它矢量、无背景、边缘干净,
        是做「品牌色圆底 + 白色字形」最好的输入,所以也不能丢。
        """
        html = ('<link rel="mask-icon" href="/pinned.svg" color="#0a0">'
                '<link rel="icon" sizes="32x32" href="/small.png">')
        ranked = site_icons.rank(site_icons.link_candidates(html, "https://e.com/"))
        self.assertEqual(urls(ranked), ["https://e.com/small.png", "https://e.com/pinned.svg"])

    def test_a_windows_tile_image_counts_as_a_designed_icon(self):
        html = '<meta name="msapplication-TileImage" content="/tile.png">'
        found = site_icons.link_candidates(html, "https://e.com/")
        self.assertEqual(urls(found), ["https://e.com/tile.png"])
        self.assertGreater(found[0].size, site_icons.MIN_DESIGNED_SIZE)

    def test_tags_without_a_recognised_rel_or_href_are_ignored(self):
        html = ('<link rel="stylesheet" href="/app.css">'
                '<link rel="icon">'
                '<link rel="preconnect" href="https://cdn.example.com">')
        self.assertEqual(site_icons.link_candidates(html, "https://e.com/"), [])


class ManifestTests(unittest.TestCase):
    def test_the_manifest_link_is_resolved(self):
        html = '<link rel="manifest" href="/site.webmanifest">'
        self.assertEqual(site_icons.manifest_url(html, "https://e.com/"),
                         "https://e.com/site.webmanifest")
        self.assertEqual(site_icons.manifest_url("<html></html>", "https://e.com/"), "")

    def test_manifest_icons_bring_in_sizes_the_html_never_declared(self):
        """PWA 站常把最大的一份只写进 manifest,HTML 里只留 apple-touch-icon。

        不读 manifest 就会白白少掉最清晰的那一枚。
        """
        payload = ('{"icons":[{"src":"/i/192.png","sizes":"192x192"},'
                   '{"src":"/i/512.png","sizes":"512x512"}]}')
        ranked = site_icons.rank(
            site_icons.manifest_candidates(payload, "https://e.com/site.webmanifest"))
        self.assertEqual(urls(ranked), ["https://e.com/i/512.png", "https://e.com/i/192.png"])

    def test_a_broken_manifest_yields_nothing_instead_of_raising(self):
        self.assertEqual(site_icons.manifest_candidates(b"<html>404</html>", "https://e.com/m"), [])
        self.assertEqual(site_icons.manifest_candidates('{"icons":"nope"}', "https://e.com/m"), [])


class RankingTests(unittest.TestCase):
    def test_a_declared_icon_beats_the_root_path_guess_at_the_same_size(self):
        """T-POWERS 是这条的证据,也是这次唯一一处回归。

        它根目录的 `/apple-touch-icon.png` 是一条带「T-POWERS」文字的横向锁定图,
        而 `<link>` 里声明的 `/assets/images/common/apple-touch-icon.png` 是紧凑标识。
        两个都是 180;按大小并列时,路径更短的字标会排到前面去。站点自己指了哪一个就听
        哪一个,根路径只是我们的猜测。
        """
        declared = site_icons.link_candidates(
            '<link rel="apple-touch-icon" sizes="180x180" '
            'href="/assets/images/common/apple-touch-icon.png">', "https://t.co/")
        ranked = site_icons.rank(declared + site_icons.conventional_candidates("https://t.co/"))
        self.assertEqual(ranked[0].url, "https://t.co/assets/images/common/apple-touch-icon.png")

    def test_the_same_url_declared_twice_keeps_the_larger_score(self):
        same = "https://e.com/icon.png"
        ranked = site_icons.rank([site_icons.Candidate(same, 16, "icon"),
                                  site_icons.Candidate(same, 180, "apple-touch")])
        self.assertEqual([(c.url, c.size) for c in ranked], [(same, 180)])

    def test_an_override_outranks_everything_the_site_declares(self):
        """FANZA:资产托在 p-smith.com,video.dmm.co.jp 自己的 `<link>` 指不到那里。"""
        ranked = site_icons.rank(
            site_icons.overrides_for("https://video.dmm.co.jp/av/content/?id=x")
            + site_icons.link_candidates(
                '<link rel="icon" type="image/svg+xml" href="/dmm.svg">', "https://video.dmm.co.jp/"))
        self.assertEqual(ranked[0].url, "https://p-smith.com/pinned/favicon_r18.ico")

    def test_fc2_takes_the_confirmed_off_site_icon_on_every_subdomain(self):
        """FC2 站上只有 16×16 的 favicon,够大的那份是横向字标——不是没找对,是没有。

        用户 2026-09-03 直接指定了这一枚(400×400、纯独角兽、没有文字)。作品链接落在
        `adult.contents.fc2.com`、`contents.fc2.com`,覆盖必须跟着子域走,否则等于没配。
        """
        for url in ["https://fc2.com/en/", "https://adult.contents.fc2.com/article/1/",
                    "https://video.fc2.com/"]:
            got = site_icons.overrides_for(url)
            self.assertTrue(got, url)
            self.assertIn("storage.googleapis.com/datanyze-data", got[0].url)
            self.assertTrue(got[0].url.endswith(
                "8ef39cbce34aece41d279b6e8e7dbb77aea3086e.png"), got[0].url)
        ranked = site_icons.rank(
            site_icons.overrides_for("https://adult.contents.fc2.com/")
            + site_icons.link_candidates(
                '<link rel="shortcut icon" href="//static.fc2.com/share/image/favicon.ico">',
                "https://adult.contents.fc2.com/"))
        self.assertIn("datanyze-data", ranked[0].url)

    def test_the_app_store_icon_is_not_offered(self):
        """用户否决了那一枚:背景多了胶片图案。留着会在下一次改动里被当成现状抄走。"""
        for entries in site_icons.HOST_OVERRIDES.values():
            for url in entries:
                self.assertNotIn("mzstatic.com", url)

    def test_overrides_match_subdomains_of_the_listed_host(self):
        self.assertTrue(site_icons.overrides_for("https://www.av-event.jp/actress/1"))
        self.assertEqual(site_icons.overrides_for("https://example.com/"), [])

    def test_a_path_keyed_override_only_matches_that_path(self):
        """BangBros 的三个频道共用一个主机。键必须带路径,否则三个频道同一枚图标。"""
        bus = site_icons.overrides_for("https://bangbros.com/websites/BangBus")
        eighteen = site_icons.overrides_for("https://bangbros.com/websites/BangBros18")
        self.assertTrue(bus and eighteen)
        self.assertNotEqual(bus[0].url, eighteen[0].url)
        for got in (bus, eighteen):
            self.assertIn("images-assets-ht.project1content.com", got[0].url)
        # 页面配置里没有 MonstersOfCock 的 logo 资产,未取得就是未取得,不编一个。
        self.assertEqual(
            site_icons.overrides_for("https://bangbros.com/websites/MonstersOfCock"), [])
        self.assertEqual(site_icons.overrides_for("https://bangbros.com/"), [])

    def test_a_trailing_slash_and_deeper_path_still_match(self):
        for url in ["https://bangbros.com/websites/BangBus/",
                    "https://www.bangbros.com/websites/BangBus?page=2",
                    "https://bangbros.com/websites/BangBus/videos/1"]:
            with self.subTest(url=url):
                self.assertTrue(site_icons.overrides_for(url), url)

    def test_the_longest_matching_prefix_wins(self):
        """主机级条目不许把路径级条目盖掉:那正是同主机坍缩的来源。"""
        table = dict(site_icons.HOST_OVERRIDES)
        table["bangbros.com"] = ("https://bangbros.com/favicon.ico",)
        table["bangbros.com/websites"] = ("https://e.com/generic.png",)
        original = site_icons.HOST_OVERRIDES
        site_icons.HOST_OVERRIDES = table
        try:
            got = site_icons.overrides_for("https://bangbros.com/websites/BangBus")
            self.assertIn("6480a83863bd03", got[0].url)
            self.assertEqual(
                site_icons.overrides_for("https://bangbros.com/websites/Other")[0].url,
                "https://e.com/generic.png")
            self.assertEqual(site_icons.overrides_for("https://bangbros.com/")[0].url,
                             "https://bangbros.com/favicon.ico")
        finally:
            site_icons.HOST_OVERRIDES = original

    def test_a_path_keyed_override_does_not_leak_to_subdomains(self):
        """带路径的键管的是一条路径,不是一族主机。"""
        self.assertEqual(
            site_icons.overrides_for("https://cdn.bangbros.com/websites/BangBus"), [])

    def test_the_host_key_ignores_www_and_case(self):
        self.assertEqual(site_icons.host_key("https://WWW.X.com/a"), "x.com")
        self.assertEqual(site_icons.host_key("not a url"), "")


class DiscoverTests(unittest.TestCase):
    def setUp(self):
        self.asked: list[str] = []

    def fetcher(self, pages):
        def fetch(url):
            self.asked.append(url)
            got = pages.get(url)
            return None if got is None else (got, "")
        return fetch

    def test_discovery_walks_homepage_then_manifest_then_conventional(self):
        fetch = self.fetcher({
            "https://e.com": b'<link rel="manifest" href="/m.json">',
            "https://e.com/m.json": b'{"icons":[{"src":"/big.png","sizes":"512x512"}]}',
        })
        found = site_icons.discover("https://e.com/talent/a", fetch)
        self.assertEqual(urls(found), ["https://e.com/big.png",
                                       "https://e.com/apple-touch-icon.png",
                                       "https://e.com/favicon.ico"])

    def test_a_homepage_that_cannot_be_fetched_still_leaves_the_old_conventions(self):
        """首页取不到不算失败:老规矩位置照样值得一试。"""
        found = site_icons.discover("https://e.com/a", self.fetcher({}))
        self.assertEqual(urls(found), ["https://e.com/apple-touch-icon.png",
                                       "https://e.com/favicon.ico"])

    def test_a_url_without_an_origin_asks_nothing(self):
        self.assertEqual(site_icons.discover("not a url", self.fetcher({})), [])
        self.assertEqual(self.asked, [])

    def test_everything_discovery_finds_is_only_host_scoped(self):
        """发现流程从 `origin(url)` 出发,路径一丢,结果只能代表主机。

        `bangbros.com/websites/{BangBus,BangBros18,MonstersOfCock}` 三个频道因此拿到
        逐字相同的候选表——这就是三份结果 sha256 完全一样的来源。
        """
        fetch = self.fetcher({
            "https://network.example": b'<link rel="icon" sizes="16x16" href="/fav16.png">'})
        found = [site_icons.discover(f"https://network.example/websites/{name}", fetch)
                 for name in ("ChannelA", "ChannelB", "ChannelC")]
        self.assertEqual(urls(found[0]), urls(found[1]))
        self.assertEqual(urls(found[1]), urls(found[2]))
        self.assertEqual({c.scope for c in found[0]}, {site_icons.HOST_SCOPE})

    def test_an_override_candidate_is_entity_scoped(self):
        got = site_icons.overrides_for("https://bangbros.com/websites/BangBus")
        self.assertEqual([c.scope for c in got], [site_icons.ENTITY_SCOPE])


class ArchiveViewTests(unittest.TestCase):
    """存档链接的圆标按原站找:图标是那家公司的,取图走同一份快照。"""

    SNAPSHOT = ("https://web.archive.org/web/20220512171503/"
                "https://ones-double.com/people/mikami-yua/")

    def test_a_snapshot_is_looked_up_as_the_site_it_preserved(self):
        page, locate = social_links.archive_view(self.SNAPSHOT)
        self.assertEqual(page, "https://ones-double.com/people/mikami-yua/")
        self.assertEqual(locate("https://ones-double.com/favicon.ico"),
                         "https://web.archive.org/web/20220512171503id_/"
                         "https://ones-double.com/favicon.ico")

    def test_an_address_already_inside_the_archive_is_fetched_as_is(self):
        _, locate = social_links.archive_view(self.SNAPSHOT)
        pinned = "https://web.archive.org/web/20220130000436id_/https://ones-double.com/logo.svg"
        self.assertEqual(locate(pinned), pinned)

    def test_a_live_link_is_left_alone(self):
        page, locate = social_links.archive_view("https://faleno.jp/top/")
        self.assertEqual((page, locate("https://faleno.jp/favicon.ico")),
                         ("https://faleno.jp/top/", "https://faleno.jp/favicon.ico"))

    def test_the_archive_front_page_is_not_a_snapshot(self):
        page, _ = social_links.archive_view("https://web.archive.org/")
        self.assertEqual(page, "https://web.archive.org/")


def reject_host_scope(candidate, data, content_type=""):
    """`harvest_studio_icons` 挂的那道守卫,这里用最小实现代表它。"""
    return candidate.scope == site_icons.ENTITY_SCOPE


class SharedHostGuardTests(unittest.TestCase):
    """同主机的几个频道不能共用一枚 favicon,即使那一枚过了所有闸门。"""

    def fetcher(self, extra=None):
        pages = {
            # 频道页只声明 16×16,被 harvest 的 MIN_SHORT_EDGE 退回。
            "https://bangbros.com": b'<link rel="icon" sizes="16x16" href="/fav16.png">',
            "https://bangbros.com/fav16.png": b"SMALL",
            # Aylo 站点模板的通用图标:64×64、内容比 1.00,两道闸门都过。
            "https://bangbros.com/apple-touch-icon.png": b"TEMPLATE-1",
            "https://bangbros.com/favicon.ico": b"TEMPLATE-1",
        }
        pages.update(extra or {})
        return lambda url: (pages[url], "") if url in pages else None

    def test_without_the_guard_the_platform_favicon_wins(self):
        """这是修之前的行为,留着当对照:闸门全过,图也不糊,只是不是这个频道的。"""
        made = site_icons.best_mark("https://bangbros.com/websites/MonstersOfCock",
                                    self.fetcher(), lambda data, content_type="": data)
        self.assertEqual(made, b"SMALL")

    def test_the_guard_refuses_every_host_level_candidate(self):
        made = site_icons.best_mark("https://bangbros.com/websites/MonstersOfCock",
                                    self.fetcher(), lambda data, content_type="": data,
                                    accept=reject_host_scope)
        self.assertIsNone(made)

    def test_the_guard_never_lets_a_refused_candidate_reach_the_fallback(self):
        """回落也不许用:那会把整站通用的 favicon 装成这个实体的标识。"""
        used = []
        made = site_icons.best_mark(
            "https://bangbros.com/websites/MonstersOfCock", self.fetcher(),
            lambda data, content_type="": None,
            fallback=lambda data, content_type="": used.append(data) or b"PLAIN",
            accept=reject_host_scope)
        self.assertIsNone(made)
        self.assertEqual(used, [])

    def test_an_override_gets_through_the_guard(self):
        """加了这个频道自己的来源,同一道守卫就该放行。"""
        override = site_icons.overrides_for("https://bangbros.com/websites/BangBus")[0].url
        made = site_icons.best_mark(
            "https://bangbros.com/websites/BangBus",
            self.fetcher({override: b"CHANNEL-LOGO"}),
            lambda data, content_type="": data, accept=reject_host_scope)
        self.assertEqual(made, b"CHANNEL-LOGO")

    def test_the_two_channels_with_overrides_get_different_bytes(self):
        """根因就是三份结果 sha256 完全相同。两个频道必须拿到各自那一份。"""
        made = []
        for name in ("BangBus", "BangBros18"):
            url = f"https://bangbros.com/websites/{name}"
            override = site_icons.overrides_for(url)[0].url
            made.append(site_icons.best_mark(
                url, self.fetcher({override: f"BYTES:{name}".encode()}),
                lambda data, content_type="": data, accept=reject_host_scope))
        self.assertEqual(made, [b"BYTES:BangBus", b"BYTES:BangBros18"])


class BestMarkTests(unittest.TestCase):
    def test_missav_uses_the_confirmed_icon_when_the_homepage_is_unavailable(self):
        icon = "https://missav.ws/img/favicon.png"
        for host in ("missav.com", "missav.ws"):
            with self.subTest(host=host):
                fetched = []

                def fetch(url, fetched=fetched):
                    fetched.append(url)
                    return (b"MISSAV-ICON", "image/png") if url == icon else None

                made = site_icons.best_mark(
                    f"https://{host}/", fetch,
                    lambda data, content_type="": data if content_type == "image/png" else None)
                self.assertEqual(made, b"MISSAV-ICON")
                self.assertIn(icon, fetched)

    def test_the_first_candidate_that_renders_wins(self):
        pages = {"https://e.com": b'<link rel="icon" sizes="256x256" href="/a.png">',
                 "https://e.com/a.png": b"A",
                 "https://e.com/apple-touch-icon.png": b"B"}
        rendered = []

        def render(data, content_type=""):
            rendered.append(data)
            return None if data == b"A" else b"MARK"

        made = site_icons.best_mark("https://e.com/x", lambda u: (pages[u], "") if u in pages else None, render)
        self.assertEqual(made, b"MARK")
        self.assertEqual(rendered, [b"A", b"B"])

    def test_the_fallback_reuses_bytes_already_fetched(self):
        """一个都没做成时才回落,且用循环里存下的第一份,不重新发现一遍。

        重新发现要把首页和 manifest 再取一次;为一张回落图打两轮请求不值得。
        """
        pages = {"https://e.com": b'<link rel="icon" sizes="256x256" href="/a.png">',
                 "https://e.com/a.png": b"A"}
        asked = []

        def fetch(url):
            asked.append(url)
            return (pages[url], "") if url in pages else None

        made = site_icons.best_mark("https://e.com/x", fetch,
                                    lambda *a, **k: None,
                                    fallback=lambda data, content_type="": b"PLAIN:" + data)
        self.assertEqual(made, b"PLAIN:A")
        self.assertEqual(asked.count("https://e.com"), 1, "首页只该取一次")

    def test_a_candidate_that_cannot_be_fetched_does_not_burn_an_attempt(self):
        """404 不算「试过一个站点资产」;预算是留给真的取回来了却不合格的那些。"""
        asked = []

        def fetch(url):
            asked.append(url)
            return (b"", "") if url == "https://e.com" else None

        self.assertIsNone(site_icons.best_mark("https://e.com/x", fetch, lambda *a, **k: b"M"))
        self.assertEqual(asked, ["https://e.com", "https://e.com/apple-touch-icon.png",
                                 "https://e.com/favicon.ico"])

    def test_the_fetch_budget_stops_a_site_with_dozens_of_declared_icons(self):
        """mgstage 声明了五个 apple-touch-icon。排序已经把最可能的放前面了,
        试到第五个还不成多半是这个站没有能用的图标,继续只是白打人家的服务器。
        """
        declared = "".join(f'<link rel="icon" sizes="{n}x{n}" href="/i{n}.png">'
                           for n in (512, 256, 192, 180, 152, 144, 128))
        tried = []

        def fetch(url):
            tried.append(url)
            return (declared.encode(), "") if url == "https://e.com" else (b"x", "")

        self.assertIsNone(site_icons.best_mark("https://e.com/a", fetch, lambda *a, **k: None))
        self.assertEqual(len(tried) - 1, site_icons.MAX_FETCH)


if __name__ == "__main__":
    unittest.main()