Ctrl K
ComunidadGuías…

tests/test_entry_links.py

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

Archivo original ↗Preguntar a KiBot
"""人物资料页的外部入口:地址怎么拼、摆在哪个位置、什么时候不出。"""
from __future__ import annotations

import json
import tempfile
import unittest
from pathlib import Path
from urllib.parse import quote

from peach import entry_links


def ref(provider: str, external_id: str, kind: str = "performer") -> dict:
    return {"provider": provider, "external_kind": kind, "external_id": external_id}


def alias(written: str, source: str = "r18:performer") -> dict:
    return {"alias": written, "source": source}


class EntryLinkAddressTests(unittest.TestCase):
    def test_each_site_lands_in_the_slot_it_belongs_to(self):
        rows = entry_links.build(entry_links.defaults(), "河合あすな",
                                 [ref("javdb", "QDvG"), ref("minnano-av", "155084")])
        self.assertEqual([row["site"] for row in rows],
                         ["minnano-av", "javdb", "missav"])
        # minnano-av 是一份资料页,排进上面那排链接;看片那两枚自己一行。
        self.assertEqual([row["slot"] for row in rows], ["pill", "mark", "mark"])
        self.assertEqual(rows[0]["url"], "https://www.minnano-av.com/actress155084.html")
        self.assertEqual(rows[1]["url"], "https://javdb.com/actors/QDvG")
        # 药丸上写站点自己的名字,取自 minnano-av 官网的 title。
        self.assertEqual([row["label"] for row in rows],
                         ["みんなのAV", "JavDB", "MISSAV"])
        # MISSAV 没有可用的图形标识,那一枚由前端按它站上的排版规则排字。
        self.assertEqual([row["mark"] for row in rows],
                         ["brand-minnano", "mark-javdb", ""])

    def test_two_pages_on_the_same_site_each_get_an_entry(self):
        """javdb 上同一位女优有两个演员页,两边挂的作品不同,都要能点进去。"""
        rows = entry_links.build(entry_links.defaults(), "释爱丽丝",
                                 [ref("javdb", "d45k9"), ref("javdb", "ZX5z7")],
                                 [alias("釈アリス", "avdb-actor-mapping@8e2d")])
        pages = [row for row in rows if row["site"] == "javdb"]
        self.assertEqual([row["label"] for row in pages], ["JavDB", "JavDB ②"])
        self.assertEqual([row["ordinal"] for row in pages], ["", "②"])
        self.assertEqual([row["url"] for row in pages],
                         ["https://javdb.com/actors/d45k9",
                          "https://javdb.com/actors/ZX5z7"])

    def test_the_page_credited_on_her_works_comes_before_a_name_backfilled_one(self):
        """#271:旧艺名那页(白石アイリ)常年是空的,排第一会让人以为链接错了。"""
        backfilled = ref("javdb", "MmbDR") | {"metadata_json": json.dumps({"source": "backfill_performer_entry_ids"})}
        credited = ref("javdb", "bkxd") | {"metadata_json": json.dumps({"source": "javdb", "provider_id": "NNPJ-466"})}
        rows = entry_links.build(entry_links.defaults(), "森日向子", [backfilled, credited])
        self.assertEqual([row["url"] for row in rows if row["site"] == "javdb"],
                         ["https://javdb.com/actors/bkxd", "https://javdb.com/actors/MmbDR"])

    def test_a_name_matched_page_of_the_same_record_as_the_credited_one_is_dropped(self):
        """#271:署名那页与按名字对上的那页都是有碼记录时,她在这一类只有署名那页。

        白石アイリ 那页是旧艺名的空页,点开就是查无此人;订阅新作同样不能去拉它。
        """
        def page(external_id, **metadata):
            return ref("javdb", external_id) | {"metadata_json": json.dumps(metadata)}

        refs = [page("MmbDR", source="backfill_performer_entry_ids", uncensored=False),
                page("bkxd", source="javdb", provider_id="NNPJ-466", uncensored=False)]
        rows = entry_links.build(entry_links.defaults(), "森日向子", refs)
        self.assertEqual([(row["label"], row["url"]) for row in rows if row["site"] == "javdb"],
                         [("JavDB", "https://javdb.com/actors/bkxd")])
        self.assertEqual(entry_links.javdb_actor_pages("森日向子", refs), ["https://javdb.com/actors/bkxd"])

    def test_name_matched_pages_stay_when_the_record_differs_or_cannot_be_told(self):
        def page(external_id, **metadata):
            return ref("javdb", external_id) | {"metadata_json": json.dumps(metadata)}

        backfill = {"source": "backfill_performer_entry_ids"}
        cases = {
            # 署名的是有碼那条,按名字对上的是同一人的無碼那条:两页都是她的。
            "另一类记录": ([page("E26vd", **backfill, uncensored=True),
                          page("pRMq", source="javdb", provider_id="MIDV-001", uncensored=False)],
                         ["pRMq", "E26vd"]),
            # 没记下记录类型,分不出是不是同一类。
            "类型未知": ([page("MmbDR", **backfill),
                        page("bkxd", source="javdb", provider_id="NNPJ-466", uncensored=False)],
                       ["bkxd", "MmbDR"]),
            # 同一类只有按名字对上的几页,没有署名可比,交给用户点开核对。
            "没有署名": ([page("GydD", **backfill, uncensored=False),
                        page("ZzOA", **backfill, uncensored=False)],
                       ["GydD", "ZzOA"]),
        }
        for name, (refs, expected) in cases.items():
            with self.subTest(name):
                self.assertEqual(entry_links.provider_ids(refs)["javdb"], expected)

    def test_a_site_without_an_id_is_absent_instead_of_a_search_address(self):
        rows = entry_links.build(entry_links.defaults(), "深田えいみ",
                                 [ref("r18dev", "1041521")])
        self.assertEqual([row["site"] for row in rows], ["missav"])
        self.assertNotIn("search", rows[0]["url"])

    def test_a_performer_outside_the_jav_directories_gets_no_watch_row(self):
        """`145cm色白お嬢様` 这类 FC2 个人摄创作者,账本 kind 也是 performer。

        JavDB 与 MISSAV 只收商业 AV 女优;本机 Stash 的 id 与 r18 的名字映射每个人都有,
        它们不是「别处收录过她」的证据,所以不能拿来开这一行。
        """
        rows = entry_links.build(entry_links.defaults(), "145cm色白お嬢様",
                                 [ref("stash", "912"),
                                  ref("r18", "145cm", kind="performer_name")])
        self.assertEqual(rows, [])

    def test_an_agency_roster_id_does_not_open_the_watch_row(self):
        """K-MIB 那类事务所站收的是所属艺人,不是 JAV 的作品目录。

        名册上有她,说明这家公司签了她;她拍的是不是商业 AV、JavDB 与 MISSAV 上有没有
        她的页面,这条引用一个字都没说。
        """
        rows = entry_links.build(entry_links.defaults(), "神木ほのか",
                                 [ref("kmib", "218")])
        self.assertEqual(rows, [])

    def test_missav_uses_the_japanese_stage_name(self):
        """规范名是中文译名时,MISSAV 那条按别名里的日文艺名拼。

        中文译名与纯平假名读音不作为混合文字艺名的入口。
        """
        rows = entry_links.build(entry_links.defaults(), "释爱丽丝",
                                 [ref("r18dev", "1096544")], [
            alias("Alice Shaku", "avdb-actor-mapping@8e2d"),
            alias("しゃくありす", "r18:performer"),
            alias("釈アリス", "avdb-actor-mapping@8e2d"),
        ])
        self.assertEqual(
            rows[0]["url"],
            "https://missav.ws/dm3/cn/actresses/%E9%87%88%E3%82%A2%E3%83%AA%E3%82%B9")

    def test_current_japanese_name_wins_over_a_former_name_alias(self):
        rows = entry_links.build(entry_links.defaults(), "逢沢みゆ",
                                 [ref("javdb", "zK98J")], [
            alias("あいざわみゆ", "r18dev:performer-profile"),
            alias("坂本マリア", "auto:performer-alias@2048"),
        ])
        self.assertEqual(next(row["url"] for row in rows if row["site"] == "missav"),
                         "https://missav.ws/dm3/cn/actresses/"
                         "%E9%80%A2%E6%B3%BD%E3%81%BF%E3%82%86")

    def test_plain_hiragana_canonical_reading_uses_a_stage_name_alias(self):
        self.assertEqual(entry_links.japanese_name("あいざわみゆ", [alias("逢沢みゆ")]),
                         "逢沢みゆ")

    def test_glyph_equivalent_alias_preserves_the_japanese_spelling(self):
        self.assertEqual(entry_links.japanese_name("蓝川みれい", [
            alias("藍川みれい", "avdb-actor-mapping"), alias("坂本マリア"),
        ]), "藍川みれい")

    def test_plain_hiragana_alias_is_not_a_han_stage_name(self):
        for name, reading in (("三宅美香", "みやけみか"), ("音海里奈", "おとみりな")):
            with self.subTest(name=name):
                self.assertEqual(entry_links.japanese_name(name, [alias(reading)]), name)

    def test_a_published_hiragana_stage_name_is_distinct_from_a_profile_reading(self):
        aliases = [alias("なぎさ", "javdb-actor-page")]
        self.assertEqual(entry_links.japanese_name("渚向日葵", aliases), "なぎさ")
        self.assertEqual(entry_links.japanese_name("渚向日葵", aliases,
                         {"source": "javdb-actor-page", "jp": "なぎさ", "zh_cn": "渚向日葵"}),
                         "なぎさ")

    def test_glyph_equivalent_chinese_alias_does_not_hide_a_published_kana_stage_name(self):
        for name, jp, chinese in (("逢泽美优", "逢沢みゆ", "逢澤美優"),
                                  ("优梨舞奈", "優梨まいな", "優梨舞奈"),
                                  ("新井爱丽", "新井エリー", "新井愛麗")):
            with self.subTest(name=name):
                aliases = [alias(chinese, "review:performer-localization"),
                           alias(jp, "review:performer-localization")]
                self.assertEqual(entry_links.japanese_name(name, aliases,
                                 {"source": "review:performer-localization", "jp": jp, "zh_cn": name}), jp)

    def test_equivalent_han_name_preserves_original_glyphs_and_iteration_mark(self):
        for name, jp in (("北条麻妃", "北條麻妃"), ("乃乃果花", "乃々果花"),
                         ("梨梨衣", "梨々衣")):
            with self.subTest(name=name):
                self.assertEqual(entry_links.japanese_name(name, [alias(jp)]), jp)

    def test_missav_preserves_source_stage_name_glyphs(self):
        from urllib.parse import unquote, urlsplit

        for name, jp in (("七泽美亚", "七沢みあ"), ("逢泽美优", "逢沢みゆ"),
                         ("北条麻妃", "北條麻妃"), ("乃乃果花", "乃々果花"),
                         ("优梨舞奈", "優梨まいな")):
            with self.subTest(name=name):
                directory = entry_links.directory_index({"source": entry_links.DIRECTORY_URL,
                    "entries": [{"name": name, "url": "https://missav.ws/dm3/cn/actresses/" + quote(jp)}]})
                rows = entry_links.build(entry_links.defaults(), name,
                                         [ref("javdb", "sample")], [alias(jp)], directory=directory)
                address = next(row["url"] for row in rows if row["site"] == "missav")
                self.assertEqual(unquote(urlsplit(address).path), f"/dm3/cn/actresses/{jp}")

    def test_paired_current_han_name_wins_over_an_unreviewed_former_name(self):
        localized = {"source": "avdb-actor-mapping", "jp": "水谷心音", "zh_cn": "水谷心音"}
        rows = entry_links.build(entry_links.defaults(), "水谷心音", [ref("javdb", "sample")],
                                 [alias("藤崎りお"), alias("みずたにここね")], localized)
        self.assertEqual(next(row["url"] for row in rows if row["site"] == "missav"),
                         "https://missav.ws/dm3/cn/actresses/"
                         "%E6%B0%B4%E8%B0%B7%E5%BF%83%E9%9F%B3")

    def test_localization_conflicts_do_not_replace_the_current_identity(self):
        self.assertEqual(entry_links.japanese_name("霞理沙", [alias("かすみりさ")],
                         {"source": "avdb-actor-mapping", "jp": "松冈仁美", "zh_cn": "霞理沙"}),
                         "霞理沙")
        self.assertEqual(entry_links.japanese_name("泽村玲子", [
            alias("澤村レイコ", "avdb-actor-mapping")],
            {"source": "avdb-actor-mapping", "jp": "泽村玲子", "zh_cn": "泽村玲子"}),
                         "澤村レイコ")
        self.assertEqual(entry_links.japanese_name("鹫尾芽衣", [
            alias("筧ジュン", "avdb-actor-mapping"), alias("鷲尾めい", "avdb-actor-mapping")],
            {"source": "avdb-actor-mapping", "jp": "鷲尾めい", "zh_cn": "鹫尾芽衣"}),
                         "筧ジュン")

    def test_paired_han_name_requires_the_same_current_chinese_name(self):
        self.assertEqual(entry_links.japanese_name("宇野刊菜", [alias("あべかんな")],
                         {"source": "avdb-actor-mapping", "jp": "宇野栞菜", "zh_cn": "宇野刊菜"}),
                         "宇野栞菜")
        self.assertEqual(entry_links.japanese_name("宇野刊菜", [alias("あべかんな")],
                         {"source": "avdb-actor-mapping", "jp": "宇野栞菜", "zh_cn": "别的写法"}),
                         "宇野刊菜")

    def test_a_reading_in_plain_hiragana_loses_to_the_written_stage_name(self):
        rows = entry_links.build(entry_links.defaults(), "天川空",
                                 [ref("r18dev", "1091104")], [
            alias("あまかわそら", "r18:performer"),
            alias("天川そら", "avdb-actor-mapping@8e2d"),
        ])
        self.assertTrue(rows[0]["url"].endswith("/%E5%A4%A9%E5%B7%9D%E3%81%9D%E3%82%89"))

    def test_a_stage_name_source_wins_among_writings_of_the_same_shape(self):
        rows = entry_links.build(entry_links.defaults(), "佐佐木纱希",
                                 [ref("r18dev", "1075533")], [
            alias("佐々木サキ", "stash:performer"),
            alias("佐々木さき", "avdb-actor-mapping@8e2d"),
        ])
        self.assertTrue(rows[0]["url"].endswith(
            "/%E4%BD%90%E3%80%85%E6%9C%A8%E3%81%95%E3%81%8D"))

    def test_the_canonical_name_stands_in_when_no_alias_reads_as_japanese(self):
        rows = entry_links.build(entry_links.defaults(), "楪 カレン",
                                 [ref("r18dev", "1058712")],
                                 [alias("Karen Yuzuriha", "r18:performer")])
        self.assertEqual(
            rows[0]["url"],
            "https://missav.ws/dm3/cn/actresses/%E6%A5%AA%20%E3%82%AB%E3%83%AC%E3%83%B3")

    def test_ids_from_another_entity_kind_do_not_count(self):
        # 事务所那 55 条 minnano-av 引用记的是 `production`,不是这个人在站上的号;
        # 它既拼不出 minnano 的资料页,也不足以说明这个人是 JAV 女优。
        rows = entry_links.build(entry_links.defaults(), "七沢みあ",
                                 [ref("minnano-av", "110", kind="production")])
        self.assertEqual(rows, [])

    def test_a_mirror_domain_replaces_only_the_host(self):
        settings = entry_links.defaults()
        settings["javdb"]["host"] = "javdb521.com"
        rows = entry_links.build(settings, "七沢みあ", [ref("javdb", "NPD3")])
        self.assertEqual(rows[0]["url"], "https://javdb521.com/actors/NPD3")

    def test_the_feed_address_stays_on_javdb_whatever_the_mirror(self):
        """订阅地址按主机计限流预算,跟着镜像走就绕开了 `javdb.com` 那份预算。"""
        self.assertEqual(entry_links.javdb_actor_pages("七沢みあ", [ref("javdb", "NPD3")]),
                         ["https://javdb.com/actors/NPD3"])
        self.assertEqual(entry_links.javdb_actor_pages(
            "145cm色白お嬢様", [ref("stash", "12")]), [])

    def test_a_broken_domain_falls_back_to_the_default_site(self):
        """手改坏的域名拼出来是个不存在的主机名,那比退回默认站更难看出问题。"""
        settings = entry_links.defaults()
        settings["javdb"]["host"] = "javdb.com/actors"
        rows = entry_links.build(settings, "七沢みあ", [ref("javdb", "NPD3")])
        self.assertEqual(rows[0]["url"], "https://javdb.com/actors/NPD3")

    def test_an_empty_domain_means_the_default_one(self):
        """配置页那个框默认就是空的:留空是「没换过」,不是「没有地址」。"""
        rows = entry_links.build({"javdb": {"host": ""}}, "七沢みあ", [ref("javdb", "NPD3")])
        self.assertEqual(rows[0]["url"], "https://javdb.com/actors/NPD3")


class EntryLinkSettingsFileTests(unittest.TestCase):
    def setUp(self):
        temporary = tempfile.TemporaryDirectory()
        self.addCleanup(temporary.cleanup)
        self.root = Path(temporary.name).resolve()

    def body(self, **changed: dict) -> dict:
        """配置页提交的形状:能换域名的那几站各一行,值就是框里写的那串。"""
        sites = {site.key: {"host": ""} for site in entry_links.SITES if site.mirrored}
        sites.update(changed)
        return {"sites": sites}

    def test_an_absent_file_reads_as_no_domain_changed(self):
        """配置页只管能换域名的那两站;みんなのAV 只此一家,不在这份设置里。"""
        saved = entry_links.read(self.root)
        self.assertEqual(sorted(saved), ["javdb", "missav"])
        self.assertTrue(all(row["host"] == "" for row in saved.values()))

    def test_saving_round_trips_through_the_snapshot(self):
        entry_links.save(self.root, self.body(javdb={"host": "javdb521.com"}))
        rows = {row["key"]: row for row in entry_links.snapshot(self.root)["sites"]}
        self.assertEqual(sorted(rows), ["javdb", "missav"])
        self.assertEqual(rows["javdb"]["host"], "javdb521.com")
        self.assertEqual(rows["javdb"]["default_host"], "javdb.com")
        self.assertEqual(rows["javdb"]["label"], "JavDB")
        # 没动过的那一站留空:框里显示的是占位符,不是一串要人去认的默认值。
        self.assertEqual(rows["missav"]["host"], "")
        self.assertEqual(rows["missav"]["default_host"], "missav.ws")

    def test_directory_file_supplies_the_published_missav_path(self):
        path = self.root / entry_links.DIRECTORY_FILENAME
        first = "https://missav.ws/dm102/cn/actresses/" + quote("七沢みあ")
        path.write_text(json.dumps({"source": entry_links.DIRECTORY_URL,
                                  "entries": [{"name": "七泽美亚", "url": first}]}), encoding="utf-8")
        rows = entry_links.entry_links(self.root, "七泽美亚", [ref("javdb", "NPD3")],
                                       [alias("七沢みあ")])
        self.assertEqual(next(row["url"] for row in rows if row["site"] == "missav"), first)
        second = first.replace("dm102", "dm1002")
        path.write_text(json.dumps({"source": entry_links.DIRECTORY_URL,
                                  "entries": [{"name": "七泽美亚", "url": second}]}), encoding="utf-8")
        rows = entry_links.entry_links(self.root, "七泽美亚", [ref("javdb", "NPD3")],
                                       [alias("七沢みあ")])
        self.assertEqual(next(row["url"] for row in rows if row["site"] == "missav"), second)


    def test_a_pasted_address_is_trimmed_down_to_its_domain(self):
        """镜像地址多半是整条复制过来的,前缀与末尾的斜杠不值得弹一条错误。"""
        entry_links.save(self.root, self.body(missav={"host": "https://missav.ai/"}))
        self.assertEqual(entry_links.read(self.root)["missav"]["host"], "missav.ai")

    def test_an_address_with_a_path_is_refused(self):
        with self.assertRaises(ValueError) as caught:
            entry_links.save(self.root, self.body(
                javdb={"host": "javdb.com/actors/{javdb_id}"}))
        self.assertIn("只写域名本身", str(caught.exception))

    def test_clearing_the_box_goes_back_to_the_default_domain(self):
        entry_links.save(self.root, self.body(missav={"host": "missav.ai"}))
        entry_links.save(self.root, self.body(missav={"host": "  "}))
        self.assertEqual(entry_links.read(self.root)["missav"]["host"], "")

    def test_a_hand_broken_file_falls_back_instead_of_emptying_the_row(self):
        (self.root / entry_links.FILENAME).write_text(
            json.dumps({"sites": {"javdb": {"host": "not a domain"}}}), encoding="utf-8")
        self.assertEqual(entry_links.read(self.root)["javdb"]["host"], "")


class MissavDirectoryTests(unittest.TestCase):
    def directory(self, rows, *, complete=False):
        return entry_links.directory_index({"source": entry_links.DIRECTORY_URL,
                                            "entries": rows, "complete": complete})

    def test_failed_source_page_has_no_template_fallback(self):
        address = "https://missav.ws/dm31/cn/actresses/Rina"
        for complete in (False, True):
            with self.subTest(complete=complete):
                directory = self.directory([{"name": "Rina", "url": address, "verified": False}],
                                           complete=complete)
                rows = entry_links.build(entry_links.defaults(), "RINA", [ref("javdb", "actor")],
                                         directory=directory)
                self.assertNotIn("missav", [row["site"] for row in rows])

    def test_conflicting_page_evidence_requires_a_verified_source(self):
        failed = {"name": "Rina", "url": "https://missav.ws/dm31/cn/actresses/Rina", "verified": False}
        alternate = {"name": "Rina", "url": "https://missav.ws/dm83/cn/actresses/Rina"}
        self.assertEqual(self.directory([failed, alternate]).path("Rina", "RINA"), "")
        restored = self.directory([{**failed, "verified": True}])
        self.assertEqual(restored.path("Rina", "RINA"), "/dm31/cn/actresses/Rina")

    def test_site_glyphs_and_compound_stage_names_use_the_published_href(self):
        for canonical, jp, display, slug in (
                ("北条麻妃", "北條麻妃", "北条麻妃", "北条麻妃"),
                ("森泽佳奈", "飯岡かなこ", "森泽佳奈 (饭冈佳奈子)", "森沢かな(飯岡かなこ)")):
            with self.subTest(canonical=canonical):
                address = "https://missav.ws/dm193/cn/actresses/" + quote(slug)
                directory = self.directory([{"name": display, "url": address}])
                rows = entry_links.build(entry_links.defaults(), canonical, [ref("javdb", "actor")],
                                         [alias(jp)], directory=directory)
                self.assertEqual(next(row["url"] for row in rows if row["site"] == "missav"), address)

    def test_a_han_current_name_recorded_by_directories_beats_a_former_name_alias(self):
        """#271:r18 与 avwikidb 都把「森日向子」记成她的名字,可信来源的旧艺名不能顶替。"""
        aliases = [alias("白石アイリ", "av_name@namewikis-20260924"), alias("朝比奈えみり", "auto:performer-alias@1")]
        recorded = [ref("javdb", "bkxd"), ref("r18", "森日向子", "performer_name"),
                    ref("avwikidb", "1") | {"metadata_json": json.dumps({"name": "森日向子", "kana": "もり ひなこ"})}]
        for refs in (recorded[:2], [recorded[0], recorded[2]]):
            with self.subTest(refs=[row["provider"] for row in refs]):
                self.assertEqual(entry_links.japanese_name("森日向子", aliases, refs=refs), "森日向子")
        self.assertEqual(entry_links.japanese_name("森日向子", aliases, refs=recorded[:1]), "白石アイリ")
        # 记下的是日文字形时用资料里的写法,中文规范名只用来对上她。
        self.assertEqual(entry_links.japanese_name("三上悠亚", [alias("ヒュージョン亀頭", "wiki")],
                                                   refs=[ref("r18", "三上悠亜", "performer_name")]), "三上悠亜")
        address = "https://missav.ws/dm127/cn/actresses/" + quote("森日向子")
        directory = self.directory([{"name": "森日向子", "url": address},
                                    {"name": "白石アイリ", "url": "https://missav.ws/dm31/cn/actresses/X"}], complete=True)
        rows = entry_links.build(entry_links.defaults(), "森日向子", recorded, aliases, directory=directory)
        self.assertEqual(next(row["url"] for row in rows if row["site"] == "missav"), address)

    def test_a_recorded_name_without_a_directory_page_keeps_the_alias_page(self):
        """资料记下的是她改名前的写法、名录里只有新艺名的页时,照旧链到别名挑出的那页。"""
        address = "https://missav.ws/dm62/cn/actresses/" + quote("とみの伊織")
        directory = self.directory([{"name": "とみの伊織", "url": address}], complete=True)
        rows = entry_links.build(entry_links.defaults(), "富野伊织",
                                 [ref("javdb", "a"), ref("r18", "富野伊織", "performer_name")],
                                 [alias("とみの伊織", "wiki")], directory=directory)
        self.assertEqual(next(row["url"] for row in rows if row["site"] == "missav"), address)

    def test_ambiguous_directory_names_have_no_missav_entry(self):
        directory = self.directory([
            {"name": "同名艺人", "url": "https://missav.ws/dm1/cn/actresses/A"},
            {"name": "同名艺人", "url": "https://missav.ws/dm2/cn/actresses/B"},
        ])
        rows = entry_links.build(entry_links.defaults(), "同名艺人", [ref("javdb", "actor")],
                                 directory=directory)
        self.assertEqual([row["site"] for row in rows], ["javdb"])

    def test_native_name_whitespace_preserves_the_published_url(self):
        for written, published in (("ローレン 花恋", "ローレン花恋"),
                                   ("日向\u3000なつ", "日向なつ")):
            with self.subTest(name=written):
                address = "https://missav.ws/dm193/cn/actresses/" + quote(published)
                directory = self.directory([{"name": published, "url": address}])
                rows = entry_links.build(entry_links.defaults(), written, [ref("javdb", "actor")],
                                         directory=directory)
                self.assertEqual(next(row["url"] for row in rows if row["site"] == "missav"), address)

    def test_latin_name_whitespace_keeps_names_distinct(self):
        directory = self.directory([{"name": "Rina", "url": "https://missav.ws/dm1/cn/actresses/Rina"}],
                                   complete=True)
        rows = entry_links.build(entry_links.defaults(), "Rin A", [ref("javdb", "actor")],
                                 directory=directory)
        self.assertEqual([row["site"] for row in rows], ["javdb"])

    def test_distinct_published_slugs_remain_ambiguous_with_native_whitespace(self):
        directory = self.directory([
            {"name": "日向なつ", "url": "https://missav.ws/dm1/cn/actresses/" + quote("日向なつ")},
            {"name": "日向 なつ", "url": "https://missav.ws/dm2/cn/actresses/" + quote("日向 なつ")},
        ])
        self.assertEqual(directory.path("日向なつ", "日向なつ"), "")

    def test_prefix_variants_of_the_same_published_slug_share_one_identity(self):
        directory = self.directory([
            {"name": "A", "url": "https://missav.ws/dm1/cn/actresses/A"},
            {"name": "A", "url": "https://missav.ws/dm2/cn/actresses/A"},
        ])
        self.assertEqual(directory.path("A", "A"), "/dm1/cn/actresses/A")

    def test_an_incomplete_directory_does_not_assert_an_unlisted_name_is_absent(self):
        directory = self.directory([])
        self.assertIsNone(directory.path("七沢みあ", "七泽美亚"))
        rows = entry_links.build(entry_links.defaults(), "七泽美亚", [ref("javdb", "actor")],
                                 [alias("七沢みあ")], directory=directory)
        self.assertTrue(any(row["site"] == "missav" for row in rows))

    def test_a_complete_directory_has_no_entry_for_an_unmatched_name(self):
        rows = entry_links.build(entry_links.defaults(), "同名艺人", [ref("javdb", "actor")],
                                 directory=self.directory([], complete=True))
        self.assertEqual([row["site"] for row in rows], ["javdb"])

    def test_directory_entries_are_scoped_to_the_public_actress_origin(self):
        invalid = ("http://missav.ws/cn/actresses/A", "https://missav.ws.evil.test/cn/actresses/A",
                   "https://name@missav.ws/cn/actresses/A", "https://missav.ws/cn/videos/A",
                   "https://missav.ws/cn/actresses/A/extra")
        directory = self.directory([{"name": "A", "url": value} for value in invalid])
        self.assertEqual(directory.paths, {})

    def test_a_mirror_preserves_the_directory_path(self):
        settings = entry_links.defaults()
        settings["missav"]["host"] = "missav.ai"
        directory = self.directory([{"name": "A", "url": "https://missav.ws/dm102/cn/actresses/A"}])
        rows = entry_links.build(settings, "A", [ref("javdb", "actor")], directory=directory)
        self.assertEqual(next(row["url"] for row in rows if row["site"] == "missav"),
                         "https://missav.ai/dm102/cn/actresses/A")

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